pacman::p_load(sf, sp, tidyverse, tmap, spatstat, spdep, leaflet, ggthemes, performance, data.table, reshape2, ggpubr, DT, stplanr, kableExtra, RColorBrewer, dplyr,tidyr, gridExtra, circlize)Take Home Exercise 3: Prototyping Modules for Geospatial Analytics Shiny Application
1.0 Overview
Demand for Grab’s Ride-hailing services in Jakarta
This project aims to explore the factors affecting demand for Grab’s services in the city of Jakarta through Spatial Interaction Modelling predominantly using Origin-Data Analysis. Our analysis appeals to a diverse set of stakeholders, both for consumers, corporate stakeholders and policy makers especially in Jakarta where traffic congestion is a predominant problem.
Jakarta’s increasing urbanization drives a growing need for on-demand transportation services like Grab, particularly in areas with high concentrations of POIs. However, there is limited understanding of how these points influence traffic congestion across the city. Key questions include:
Which POIs generate the most ride-hailing traffic?
How does demand vary across different times of the day, week, and seasons?
Can ride-hailing services improve access to key locations while alleviating congestion?
Answering these questions will provide insights into Jakarta’s mobility patterns and inform strategies to reduce traffic bottlenecks while maintaining access to key locations.
2.0 Division of Work
My Responsibilities:
- EDSA
Visualization by Time Clusters
Visualization by Weather Patterns (Presence or Absence of Rain)
Visualization by Most and Least popular spots for Origin and Destination Trips on the District Level
- OD Analysis (District Level)
Flow Maps
Origin and Destination Flows by Factor
Driving Type
Time
Weather
Push & Pull Analysis
3.0 The data
R Packages Used
3.1 Datasets used
Grab Posisi Data (Grab Posisi Data)
Contains GPS pings from Grab vehicles, including timestamps, route data, and vehicle type (motorcycle/car).
Provides insights into ride-hailing demand, traffic hotspots, and movement patterns around key locations in Jakarta.
Supporting Datasets
Jakarta Points of Interest (POI) (HumData): Includes office buildings, shopping malls, parks, and other key locations.
Indonesia Population Density Data (ArcGIS): Adds demographic context for understanding mobility trends.
Weather API (Weatherbit): Supplements our analysis with weather categories to allow for analysis of Grab Demand according to the presence or absence of rain.
Jakarta Map (HDX): Visualize the geographical districts of the City-State.
3.2 Aspatial Data
- Category Mapping for Points of Interest
poi_category <- read_csv("data/aspatial/mapping_poi/category_mapping.csv")Rows: 205 Columns: 2
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (2): value, category
ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
- Population (Aggregate by District)
jakarta_population_district <- read_csv("data/aspatial/population/jakarta_township_population.csv") %>%
group_by(district) %>%
summarize(
population_2019 = sum(population_2019, na.rm=TRUE)
) %>%
ungroup()Rows: 264 Columns: 5
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (4): province, city, district, township
dbl (1): population_2019
ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
head(jakarta_population_district)# A tibble: 6 × 2
district population_2019
<chr> <dbl>
1 Cakung 38710
2 Cempaka Putih 7440
3 Cengkareng 40470
4 Cilandak 16880
5 Cilincing 37471
6 Cipayung 21336
We observe that there are 44 Districts within the Area of Jakarta once aggregated to the district level.
- Trips Data
Inclusive of Origin, Destination
Time Cluster
Weather Conditions
trips <- readRDS("data/trip_data.rds")colnames(trips) [1] "trj_id" "driving_mode"
[3] "origin_time" "destination_time"
[5] "total_duration_minutes" "total_distance_km"
[7] "average_speed_kmh" "origin_rawlat"
[9] "origin_rawlng" "destination_rawlat"
[11] "destination_rawlng" "origin_lat"
[13] "origin_lng" "destination_lat"
[15] "destination_lng" "origin_province"
[17] "origin_city" "origin_district"
[19] "destination_province" "destination_city"
[21] "destination_district" "origin_datetime"
[23] "destination_datetime" "origin_day"
[25] "origin_hour" "destination_day"
[27] "destination_hour" "origin_time_cluster"
[29] "destination_time_cluster" "origin_date"
[31] "origin_weather_description" "origin_weather_description_category"
origin_trips <- st_as_sf(trips, coords = c("origin_lng", "origin_lat"), crs=4326)destination_trips <- st_as_sf(trips, coords = c("destination_lng", "destination_lat"), crs=4326)3.3 Geospatial Data
- Read the Indonesia Administrative boundary shapefile
# Step 1: Read the Indonesia administrative boundary shapefile
indonesia <- st_read(
dsn = "data/geospatial/indo_map",
layer = "idn_admbnda_adm3_bps_20200401"
)Reading layer `idn_admbnda_adm3_bps_20200401' from data source
`/Users/jezelei/jezeleii/IS415-GA/Take-Home_Exercise/TakeHomeEx03/data/geospatial/indo_map'
using driver `ESRI Shapefile'
Simple feature collection with 7069 features and 16 fields
Geometry type: MULTIPOLYGON
Dimension: XY
Bounding box: xmin: 95.01079 ymin: -11.00762 xmax: 141.0194 ymax: 6.07693
Geodetic CRS: WGS 84
- Filter for DKI Jakarta and rename it to “Jakarta”
- Exclude districts belonging to “Kepulauan Seribu”
jakarta_district <- indonesia %>%
filter(ADM1_EN == "Dki Jakarta") %>%
mutate(ADM1_EN = "Jakarta")
jakarta_district <- jakarta_district %>%
filter(ADM2_EN != "Kepulauan Seribu") # Exclude Kepulauan Seribu- We filter down the required columns and rename accordingly
jakarta_district <- jakarta_district %>%
dplyr::select(ADM1_EN, ADM2_EN, ADM3_EN, geometry)
jakarta_district <- jakarta_district %>%
rename(
province = ADM1_EN,
city = ADM2_EN,
district = ADM3_EN,
)- We change the CRS to 5580, Indonesia’s CRS
- We simplify the geometry with a smaller tolerance to reduce subsequent compute load
jakarta_district <- jakarta_district %>%
st_transform(crs = 5580) # Transform to WGS84
jakarta_district <- jakarta_district %>%
mutate(across(where(is.character), tolower))
jakarta_district_df <- jakarta_district %>%
st_drop_geometry()
jakarta_district <- jakarta_district %>%
st_simplify(dTolerance = 10.0) # Smaller tolerance for longitude/latitude data- We plot the interactive map using tmap
# Step 8: Plot the interactive map using tmap
tmap_mode("view") tmap mode set to interactive viewing
tm_shape(jakarta_district) +
tm_polygons("district", palette = "Blues",
border.col = "black", lwd = 0.5) + # Display district polygons with labels
tm_basemap("OpenStreetMap") Warning: Number of levels of the variable "district" is 44, which is larger
than max.categories (which is 30), so levels are combined. Set
tmap_options(max.categories = 44) in the layer function to show all levels.
tmap_mode('plot')tmap mode set to plotting
Calculation of Centroid:
jakarta_district_centroid <- jakarta_district %>%
mutate(
centroid = st_centroid(geometry),
centroid_lat = st_coordinates(centroid)[, 2],
centroid_lng = st_coordinates(centroid)[, 1]
)
jakarta_district_centroid$district <- str_to_title(jakarta_district_centroid$district)tmap_mode('view')tmap mode set to interactive viewing
tm_shape(jakarta_district) +
tm_polygons(alpha=0.3) +
tm_shape(jakarta_district_centroid) +
tm_dots(size = 0.1, alpha= 0.5, popup.vars=c("District" = "district"))tmap_mode('plot')tmap mode set to plotting
- Points of Interest Locations
indonesia_poi <- st_read(dsn="data/geospatial/indo_poi", layer="hotosm_idn_points_of_interest_points_shp") %>% st_transform(crs = 5580) Reading layer `hotosm_idn_points_of_interest_points_shp' from data source
`/Users/jezelei/jezeleii/IS415-GA/Take-Home_Exercise/TakeHomeEx03/data/geospatial/indo_poi'
using driver `ESRI Shapefile'
Simple feature collection with 150315 features and 17 fields
Geometry type: POINT
Dimension: XY
Bounding box: xmin: 95.04186 ymin: -11.00919 xmax: 141.0188 ymax: 6.073289
Geodetic CRS: WGS 84
jakarta_poi <- st_intersection(indonesia_poi, jakarta_district)Warning: attribute variables are assumed to be spatially constant throughout
all geometries
tmap_mode('plot')tmap mode set to plotting
tm_shape(jakarta_district) +
tm_polygons() +
tm_shape(jakarta_poi) +
tm_dots( alpha = 0.2)
4.0 Processing OD Data
For the purposes of this Take-Home Exercise, we will be experimenting with a subset of data, confining the trips dataset to a:
- certain day of week [Friday]
This filtered data will be stored as od_trips_friday for subsequent analysis. After which, we will zoom in on different levels based on the following variables:
- Weather [rain or not_rain]
- Vehicle Type [car or motorcycle]
- Points of Interest within the District [Categorical Count]
- Time Clusters
4.1 Geospatial Wrangling - Combining Trips Data with Centroid Mapping
- Filter trips for Friday
od_trips_friday <- trips %>%
filter(origin_day == "friday")- Convert Origin to SF Object
- Convert Destination to SF object
origin_sf <- od_trips_friday %>%
st_as_sf(coords = c("origin_lng", "origin_lat"), crs = 5580)
destination_sf <- od_trips_friday %>%
st_as_sf(coords = c("destination_lng", "destination_lat"), crs = 5580)- Transform jakarta_district_centroid to match CRS
- Perform spatial join for origin and destination points to match with the centroid
- Remove geometries to perform a left_join as opposed to an st_join
jakarta_district_centroid <- jakarta_district_centroid %>%
st_transform(crs = 5580)
origin_with_centroids <- st_join(origin_sf, jakarta_district_centroid, left = FALSE) %>%
select(trj_id, origin_district, driving_mode, origin_time_cluster,
origin_weather_description_category, centroid_lat_origin = centroid_lat,
centroid_lng_origin = centroid_lng)
destination_with_centroids <- st_join(destination_sf, jakarta_district_centroid, left = FALSE) %>%
select(trj_id, destination_district, centroid_lat_destination = centroid_lat,
centroid_lng_destination = centroid_lng)- Merge origin and destination data on
trj_id
origin_with_centroids_df <- origin_with_centroids %>% st_set_geometry(NULL)
destination_with_centroids_df <- destination_with_centroids %>% st_set_geometry(NULL)
od_merged <- left_join(origin_with_centroids_df, destination_with_centroids_df, by = "trj_id")Warning in left_join(origin_with_centroids_df, destination_with_centroids_df, : Detected an unexpected many-to-many relationship between `x` and `y`.
ℹ Row 1948 of `x` matches multiple rows in `y`.
ℹ Row 2720 of `y` matches multiple rows in `x`.
ℹ If a many-to-many relationship is expected, set `relationship =
"many-to-many"` to silence this warning.
od_merged <- od_merged %>%
select(trj_id, origin_district, destination_district, driving_mode, origin_time_cluster,
origin_weather_description_category, centroid_lat_origin, centroid_lng_origin,
centroid_lat_destination, centroid_lng_destination)
head(od_merged)# A tibble: 6 × 10
trj_id origin_district destination_district driving_mode origin_time_cluster
<chr> <chr> <chr> <chr> <chr>
1 10003 kelapa gading pasar rebo car morning lull
2 10043 setia budi pasar minggu car morning peak
3 10061 makasar duren sawit car morning peak
4 10072 setia budi mampang prapatan car morning peak
5 10086 kebayoran lama pasar minggu motorcycle morning lull
6 10089 grogol petamburan tanah abang car midnight lull
# ℹ 5 more variables: origin_weather_description_category <chr>,
# centroid_lat_origin <dbl>, centroid_lng_origin <dbl>,
# centroid_lat_destination <dbl>, centroid_lng_destination <dbl>
Next, we process the data by filtering out records with undefined destination districts, removing intra-zonal trips and removing duplicate or NA values:
Filtering Out undefined Origin Destination Districs (this could arise because of unmatched districts where there are trips coming to and fro outside of Jakarta). We convert it to lowercase beforehand first.
district_ids <- str_to_lower(str_trim(unique(jakarta_district_centroid$district)))
# Convert `origin_district` and `destination_district` to lowercase and trim whitespace in `od_trips_friday`
od_trips_friday <- od_trips_friday %>%
mutate(
origin_district = str_to_lower(str_trim(origin_district)),
destination_district = str_to_lower(str_trim(destination_district))
)
# Re-run the unmatched districts check
unmatched_origins <- od_trips_friday %>%
filter(!origin_district %in% district_ids) %>%
distinct(origin_district)
unmatched_destinations <- od_trips_friday %>%
filter(!destination_district %in% district_ids) %>%
distinct(destination_district)
# Display unmatched IDs
unmatched_origins# A tibble: 1 × 1
origin_district
<chr>
1 outside of jakarta
unmatched_destinations# A tibble: 1 × 1
destination_district
<chr>
1 outside of jakarta
In the next step, we check for duplicate records as well:
duplicate <- od_merged %>%
group_by_all() %>%
filter(n() > 1) %>%
ungroup()
duplicate# A tibble: 0 × 10
# ℹ 10 variables: trj_id <chr>, origin_district <chr>,
# destination_district <chr>, driving_mode <chr>, origin_time_cluster <chr>,
# origin_weather_description_category <chr>, centroid_lat_origin <dbl>,
# centroid_lng_origin <dbl>, centroid_lat_destination <dbl>,
# centroid_lng_destination <dbl>
Since the result is 0, we can proceed with creation of desire line maps after removing the points where there are invalid destination points (which likely correspond with outside_of_jakarta districts)
od_merged<- od_merged %>%
drop_na(destination_district)
head(od_merged)# A tibble: 6 × 10
trj_id origin_district destination_district driving_mode origin_time_cluster
<chr> <chr> <chr> <chr> <chr>
1 10003 kelapa gading pasar rebo car morning lull
2 10043 setia budi pasar minggu car morning peak
3 10061 makasar duren sawit car morning peak
4 10072 setia budi mampang prapatan car morning peak
5 10086 kebayoran lama pasar minggu motorcycle morning lull
6 10089 grogol petamburan tanah abang car midnight lull
# ℹ 5 more variables: origin_weather_description_category <chr>,
# centroid_lat_origin <dbl>, centroid_lng_origin <dbl>,
# centroid_lat_destination <dbl>, centroid_lng_destination <dbl>
5.0 Subset of Prototype: Visualizing Spatial Interaction
5.1 Removing Intra-zonal flows
We filter out rows where origin_district is the same as the destination_district.
od_data <- od_merged %>%
filter(origin_district != destination_district)- We ensures the district names are lowercased before proceeding to
od_data <- od_data %>%
mutate(origin_district = tolower(origin_district),
destination_district = tolower(destination_district)) %>%
select(origin_district, destination_district, origin_time_cluster, origin_weather_description_category, everything())
jakarta_district_centroid <- jakarta_district_centroid %>%
mutate(district = tolower(district))- We create origin and destination points with explicit IDs
origin_points <- od_data %>%
select(origin_district, centroid_lat_origin, centroid_lng_origin) %>%
distinct() %>%
st_as_sf(coords = c("centroid_lng_origin", "centroid_lat_origin"), crs = 5580) %>%
rename(district = origin_district)
destination_points <- od_data %>%
select(destination_district, centroid_lat_destination, centroid_lng_destination) %>%
distinct() %>%
st_as_sf(coords = c("centroid_lng_destination", "centroid_lat_destination"), crs = 5580) %>%
rename(district = destination_district)5.2 Creating Desire Lines
desire_lines <- od2line(
flow = od_data,
zones = origin_points,
destination = destination_points
)colnames(od_data) [1] "origin_district" "destination_district"
[3] "origin_time_cluster" "origin_weather_description_category"
[5] "trj_id" "driving_mode"
[7] "centroid_lat_origin" "centroid_lng_origin"
[9] "centroid_lat_destination" "centroid_lng_destination"
od_aggregated <- od_data %>%
group_by(origin_district, destination_district) %>%
summarise(trip_count = n_distinct(trj_id), .groups = "drop")
# View the result
head(od_aggregated)# A tibble: 6 × 3
origin_district destination_district trip_count
<chr> <chr> <int>
1 cakung cempaka putih 2
2 cakung cipayung 2
3 cakung duren sawit 5
4 cakung gambir 1
5 cakung jatinegara 5
6 cakung johar baru 1
We save the output in rds format for future use:
write_rds(od_data, "data/rds/od_data.rds")We import and save the rds into our R environment:
od_data <- read_rds ("data/rds/od_data.rds")5.2 Visualizing Desire Lines by Time Cluster
We first create a BASEMAP FOR FRIDAY TRIPS
desire_lines_filtered <- desire_lines %>%
left_join(od_aggregated, by = c("origin_district", "destination_district")) %>%
filter(trip_count > 5)
tmap_mode('view')tmap mode set to interactive viewing
tm_shape(jakarta_district) +
tm_polygons(alpha = 0.3) +
tm_shape(jakarta_district_centroid) +
tm_dots(size = 0.1,
alpha = 0.5,
popup.vars = c("District" = "district")) +
tm_shape(desire_lines_filtered) +
tm_lines(
col = "trip_count",
palette = "viridis",
lwd = 1,
alpha = 0.5,
popup.vars = c("origin_district", "destination_district", "trip_count")
) +
tm_layout(legend.outside = TRUE)tmap_mode('plot')tmap mode set to plotting
For the purposes of dynamic visualization, we avoid sticking with light default colours of tmap and an alternative propsed as seen from above is using the viridis palette.
From here, we can visualize the desire line maps based on the different variables mentioned earlier : 1. Time Cluster 2. Weather Category 3. Vehicle Type
TIME CLUSTER
colnames(od_data) [1] "origin_district" "destination_district"
[3] "origin_time_cluster" "origin_weather_description_category"
[5] "trj_id" "driving_mode"
[7] "centroid_lat_origin" "centroid_lng_origin"
[9] "centroid_lat_destination" "centroid_lng_destination"
desire_lines_mid_lull <- desire_lines %>%
left_join(od_aggregated, by = c("origin_district", "destination_district")) %>%
filter(origin_time_cluster == "midnight lull") %>%
filter(trip_count > 10)
desire_lines_mid_peak <- desire_lines %>%
left_join(od_aggregated, by = c("origin_district", "destination_district")) %>%
filter(origin_time_cluster == "midnight peak") %>%
filter(trip_count > 10)
desire_lines_morn_lull <- desire_lines %>%
left_join(od_aggregated, by = c("origin_district", "destination_district")) %>%
filter(origin_time_cluster == "morning lull") %>%
filter(trip_count > 10)
desire_lines_morn_peak <- desire_lines %>%
left_join(od_aggregated, by = c("origin_district", "destination_district")) %>%
filter(origin_time_cluster == "morning peak") %>%
filter(trip_count > 10)
desire_lines_aft_lull <- desire_lines %>%
left_join(od_aggregated, by = c("origin_district", "destination_district")) %>%
filter(origin_time_cluster == "afternoon lull") %>%
filter(trip_count > 10)
desire_lines_aft_peak <- desire_lines %>%
left_join(od_aggregated, by = c("origin_district", "destination_district")) %>%
filter(origin_time_cluster == "afternoon peak") %>%
filter(trip_count > 10)
desire_lines_evn_lull <- desire_lines %>%
left_join(od_aggregated, by = c("origin_district", "destination_district")) %>%
filter(origin_time_cluster == "evening lull") %>%
filter(trip_count > 10)
desire_lines_evn_peak <- desire_lines %>%
left_join(od_aggregated, by = c("origin_district", "destination_district")) %>%
filter(origin_time_cluster == "evening peak") %>%
filter(trip_count > 10)
tmap_mode('plot')tmap mode set to plotting
tm_shape(jakarta_district) +
tm_polygons(alpha = 0.5) +
tm_shape(jakarta_district_centroid) +
tm_dots(size = 0.3,
alpha = 0.6,
popup.vars = c("District" = "district")) +
tm_shape(desire_lines_mid_lull) +
tm_lines(
lwd = "trip_count",
scale = c(1,2,5,7),
alpha = 0.5,
popup.vars = c("origin_district", "destination_district", "trip_count")
) +
tm_basemap("OpenStreetMap") +
tm_layout(legend.outside = FALSE,
main.title="Desire Lines for Grab Trips at Midnight Lull",
main.title.position = "center",
main.title.size = 0.6,
frame=TRUE) +
tm_legend(position = c("RIGHT", "BOTTOM"), legend.text.size =0.8)
tm_shape(jakarta_district) +
tm_polygons(alpha = 0.5) +
tm_shape(jakarta_district_centroid) +
tm_dots(size = 0.3,
alpha = 0.6,
popup.vars = c("District" = "district")) +
tm_shape(desire_lines_mid_peak) +
tm_lines(
lwd = "trip_count",
scale = c(1,2,5,7),
alpha = 0.5,
popup.vars = c("origin_district", "destination_district", "trip_count")
) +
tm_basemap("OpenStreetMap") +
tm_layout(legend.outside = FALSE,
main.title="Desire Lines for Grab Trips at Midnight Peak",
main.title.position = "center",
main.title.size = 0.6,
frame=TRUE) +
tm_legend(position = c("RIGHT", "BOTTOM"), legend.text.size =0.8)
tm_shape(jakarta_district) +
tm_polygons(alpha = 0.5) +
tm_shape(jakarta_district_centroid) +
tm_dots(size = 0.3,
alpha = 0.6,
popup.vars = c("District" = "district")) +
tm_shape(desire_lines_morn_lull) +
tm_lines(
lwd = "trip_count",
scale = c(1,2,5,7),
alpha = 0.5,
popup.vars = c("origin_district", "destination_district", "trip_count")
) +
tm_basemap("OpenStreetMap") +
tm_layout(legend.outside = FALSE,
main.title="Desire Lines for Grab Trips at Midnight Lull",
main.title.position = "center",
main.title.size = 0.6,
frame=TRUE) +
tm_legend(position = c("RIGHT", "BOTTOM"), legend.text.size =0.8)
tm_shape(jakarta_district) +
tm_polygons(alpha = 0.5) +
tm_shape(jakarta_district_centroid) +
tm_dots(size = 0.3,
alpha = 0.6,
popup.vars = c("District" = "district")) +
tm_shape(desire_lines_morn_peak) +
tm_lines(
lwd = "trip_count",
scale = c(1,2,5,7),
alpha = 0.5,
popup.vars = c("origin_district", "destination_district", "trip_count")
) +
tm_basemap("Esri.WorldGrayCanvas") +
tm_layout(legend.outside = FALSE,
main.title="Desire Lines for Grab Trips at Morning Peak",
main.title.position = "center",
main.title.size = 0.6,
frame=TRUE) +
tm_legend(position = c("RIGHT", "BOTTOM"), legend.text.size =0.8)
tm_shape(jakarta_district) +
tm_polygons(alpha = 0.5) +
tm_shape(jakarta_district_centroid) +
tm_dots(size = 0.3,
alpha = 0.6,
popup.vars = c("District" = "district")) +
tm_shape(desire_lines_aft_lull) +
tm_lines(
lwd = "trip_count",
scale = c(1,2,5,7),
alpha = 0.5,
popup.vars = c("origin_district", "destination_district", "trip_count")
) +
tm_basemap("OpenStreetMap") +
tm_layout(legend.outside = FALSE,
main.title="Desire Lines for Grab Trips at Afternoon Lull",
main.title.position = "center",
main.title.size = 0.6,
frame=TRUE) +
tm_legend(position = c("RIGHT", "BOTTOM"), legend.text.size =0.8)
tm_shape(jakarta_district) +
tm_polygons(alpha = 0.5) +
tm_shape(jakarta_district_centroid) +
tm_dots(size = 0.3,
alpha = 0.6,
popup.vars = c("District" = "district")) +
tm_shape(desire_lines_aft_lull) +
tm_lines(
lwd = "trip_count",
scale = c(1,2,5,7),
alpha = 0.5,
popup.vars = c("origin_district", "destination_district", "trip_count")
) +
tm_basemap("OpenStreetMap") +
tm_layout(legend.outside = FALSE,
main.title="Desire Lines for Grab Trips at Afternoon Peak",
main.title.position = "center",
main.title.size = 0.6,
frame=TRUE) +
tm_legend(position = c("RIGHT", "BOTTOM"), legend.text.size =0.8)
tm_shape(jakarta_district) +
tm_polygons(alpha = 0.5) +
tm_shape(jakarta_district_centroid) +
tm_dots(size = 0.3,
alpha = 0.6,
popup.vars = c("District" = "district")) +
tm_shape(desire_lines_evn_lull) +
tm_lines(
lwd = "trip_count",
scale = c(1,2,5,7,8,9,10,11,12),
alpha = 0.5,
popup.vars = c("origin_district", "destination_district", "trip_count")
) +
tm_basemap("OpenStreetMap") +
tm_layout(legend.outside = FALSE,
main.title="Desire Lines for Grab Trips at Evening Lull",
main.title.position = "center",
main.title.size = 0.6,
frame=TRUE) +
tm_legend(position = c("RIGHT", "BOTTOM"), legend.text.size =0.8)Legend labels were too wide. Therefore, legend.text.size has been set to 0.71. Increase legend.width (argument of tm_layout) to make the legend wider and therefore the labels larger.

tm_shape(jakarta_district) +
tm_polygons(alpha = 0.5) +
tm_shape(jakarta_district_centroid) +
tm_dots(size = 0.3,
alpha = 0.6,
popup.vars = c("District" = "district")) +
tm_shape(desire_lines_evn_peak) +
tm_lines(
lwd = "trip_count",
scale = c(0.5,1,3,7),
alpha = 0.5,
popup.vars = c("origin_district", "destination_district", "trip_count")
) +
tm_basemap("OpenStreetMap") +
tm_layout(legend.outside = FALSE,
main.title="Desire Lines for Grab Trips at Evening Peak",
main.title.position = "center",
main.title.size = 0.6,
frame=TRUE) +
tm_legend(position = c("RIGHT", "BOTTOM"), legend.text.size =0.8)
5.3 Visualizing Desire Lines by Weather Category
For urban planners, policy makers and corporate stakeholders like Grab which rely on weather data to determine prices (where we hypothesize that strong rain for example will lead to higher demand of Grab vehicles)
From here, we can visualize the desire line maps based on the different variables mentioned earlier : 1. Time Cluster 2. Weather Category 3. Vehicle Type
WEATHER CATEGORY
desire_lines_not_rain <- desire_lines %>%
left_join(od_aggregated, by = c("origin_district", "destination_district")) %>%
filter(origin_weather_description_category == "not_rain") %>% filter(trip_count > 10)
desire_lines_rain <- desire_lines %>%
left_join(od_aggregated, by = c("origin_district", "destination_district")) %>%
filter(origin_weather_description_category == "rain") %>%
filter(trip_count > 10)tmap_mode('view')tmap mode set to interactive viewing
tm_shape(jakarta_district) +
tm_polygons(alpha = 0.2) +
tm_shape(jakarta_district_centroid) +
tm_dots(size = 0.1,
alpha = 0.6,
popup.vars = c("District" = "district")) +
tm_shape(desire_lines_not_rain) +
tm_lines(
lwd = "trip_count",
scale = c(0.5,1,3,7),
alpha = 0.5,
popup.vars = c("origin_district", "destination_district", "trip_count")
) +
tm_basemap("CartoDB.Positron") +
tm_layout(legend.outside = TRUE,
main.title="Desire Lines for No Rain Conditions",
main.title.position = "center",
main.title.size = 0.6,
frame=TRUE) +
tm_view(view.legend.position = c("RIGHT", "BOTTOM"))Legend for line widths not available in view mode.
tmap_mode('plot')tmap mode set to plotting
Note: In our data processing, we aggregated weather conditions into rain and no rain for the purposes of simplification for the exercise and to optimize data processing.
tmap_mode('view')tmap mode set to interactive viewing
tm_shape(jakarta_district) +
tm_polygons(alpha = 0.2) +
tm_shape(jakarta_district_centroid) +
tm_dots(size = 0.1,
alpha = 0.6,
popup.vars = c("District" = "district")) +
tm_shape(desire_lines_rain) +
tm_lines(
lwd = "trip_count",
scale = c(0.5,1,3,7),
alpha = 0.5,
popup.vars = c("origin_district", "destination_district", "trip_count")
) +
tm_basemap("CartoDB.Positron") +
tm_layout(legend.outside = TRUE,
main.title="Desire Lines for Rain Conditions",
main.title.position = "center",
main.title.size = 0.6,
frame=TRUE) Legend for line widths not available in view mode.
tmap_mode('plot')tmap mode set to plotting
5.3.1 Graphical Distribution of Weather Conditions throughout the Time Clusters:
Besides Map interactivity, a useful visualization for new and expert users alike are accompanying bar charts to show the weather distribution for the selected day of week, in this case Fridays.
The code chunk below 1. We summarize the origin_time_cluster and origin_weather_description category 2. Plot accordingly in a comparative bar graph
rain_distribution <- od_merged %>%
group_by(origin_time_cluster, origin_weather_description_category) %>%
summarize(count = n(), .groups = "drop") %>%
ungroup() %>%
complete(origin_time_cluster, origin_weather_description_category, fill = list(count = 0))ggplot(rain_distribution, aes(y = origin_time_cluster, x = count, fill = origin_weather_description_category)) +
geom_bar(stat = "identity", position = "dodge") +
labs(
title = "Distribution of Rain and Not Rain Conditions by Time Cluster",
y = "Time Cluster",
x = "Count",
fill = "Weather Condition"
) +
theme_minimal() +
theme(axis.text.y = element_text(angle = 0, hjust = 1))
5.4 Visualizing Desire Lines by Vehicle Type
Upon further inspection of the data, a key perspective we can investigate is the distribution of trips or how vehicle type affects desire lines and demand for Grab Vehicles.
Given Indonesia’s unique context of Traffic Congestion where to curb this, it has implemented legislation such as the Odd-Even traffic policy(TL;DR: License plates ending in odd-numbers can only drive in odd-numbered days, and the same goes for even numbers). This policy was eventually exempted for Ride-hailing services such as Grab and Gojek, but such policies shed light on the dire state of traffic congestion within the country.
There is a general understanding that motorcycle taxis in traffic-congested areas within Indonesia, especially its capital, Jakarta, are much faster on the road given they can pass through narrow roads and is much more accessible in terms of price for the average Indonesian. Given its context of one of the most traffic-congested countries in the world, the prioritization of convenience and accessibility has made the use of motorbikes more prevalent in Indonesia, as compared to other countries such as Singapore where reliable public transportation may offset such demand.
The code chunk below shows the distribution of trips by vehicle type ‘cars’ and ‘motorcycle’ respectively
desire_lines_car <- desire_lines %>%
left_join(od_aggregated, by = c("origin_district", "destination_district")) %>%
filter(driving_mode == "car") %>%
filter(trip_count > 10)
desire_lines_motorcycle <- desire_lines %>%
left_join(od_aggregated, by = c("origin_district", "destination_district")) %>%
filter(driving_mode == "motorcycle") %>%
filter(trip_count > 10)
tmap_mode('plot')tmap mode set to plotting
tm_shape(jakarta_district) +
tm_polygons(alpha = 0.2) +
tm_shape(jakarta_district_centroid) +
tm_dots(size = 0.1,
alpha = 0.6,
popup.vars = c("District" = "district")) +
tm_shape(desire_lines_car) +
tm_lines(
lwd = "trip_count",
scale = c(0.5,1,3,7),
alpha = 0.5,
popup.vars = c("origin_district", "destination_district", "trip_count")
) +
tm_basemap("CartoDB.Positron") +
tm_layout(legend.outside = TRUE,
main.title="Desire Lines for Driving Mode: Car",
main.title.position = "center",
main.title.size = 0.6,
frame=TRUE) +
tm_legend(position = c("RIGHT", "BOTTOM"), legend.text.size =0.8)
tm_shape(jakarta_district) +
tm_polygons(alpha = 0.2) +
tm_shape(jakarta_district_centroid) +
tm_dots(size = 0.1,
alpha = 0.6,
popup.vars = c("District" = "district")) +
tm_shape(desire_lines_motorcycle) +
tm_lines(
lwd = "trip_count",
scale = c(0.5,1,3,7),
alpha = 0.5,
popup.vars = c("origin_district", "destination_district", "trip_count")
) +
tm_basemap("CartoDB.Positron") +
tm_layout(legend.outside = TRUE,
main.title="Desire Lines for Driving Mode: Motorcycle",
main.title.position = "center",
main.title.size = 0.6,
frame=TRUE) +
tm_legend(position = c("RIGHT", "BOTTOM"), legend.text.size =0.8)
5.5 Visualizing Distribution of POIs
Here, we can aggregate the counts of the POIs according to their categories based on their geographic distribution. We can use the centroid of the district we have calculated earlier to combine trips and jakarta_district data
- We perform spatial intersection to associate POIs with Jakarta Districts
- Join poi_category to get the category column
- Aggregate by district and category
We join the poi_category table with the jakarta_poi dataset to map the columns accordingly:
poi_jakarta_district <- st_intersection(jakarta_poi, jakarta_district) %>%
select(district, value = amenity) %>%
st_drop_geometry() Warning: attribute variables are assumed to be spatially constant throughout
all geometries
poi_category_jakarta <- poi_jakarta_district %>%
left_join(poi_category, by = "value")
poi_aggregated <- poi_category_jakarta %>%
group_by(district, category) %>%
summarise(count = n(), .groups='drop') %>%
filter(!is.na(category))
datatable(poi_aggregated)We can visualize the distribution of the POIs
poi_aggregated_wide <- poi_aggregated %>%
pivot_wider(
names_from = category,
values_from = count,
values_fill = 0 # Fill missing values with 0
)
jakarta_district_counts <- jakarta_district %>%
left_join(poi_aggregated_wide, by = "district")
category_maps <- names(poi_aggregated_wide)[-1] # Exclude 'district' column
map_list <- lapply(category_maps, function(category) {
tm_shape(jakarta_district_counts) +
tm_polygons(
col = category,
palette = "Blues",
title = paste(category),
style = "quantile"
) +
tm_layout(main.title = paste(category), main.title.size = 1.2)
})We establish common breaks to standardize the legend values across the different maps as opposed to taking the automatic legend values based on quantiles per map. This is to facilitate ease of comparison and visualization for the User.
common_breaks <- c(0, 20, 40, 60, 80, 100, max(poi_aggregated_wide[-1], na.rm = TRUE))
map_list <- lapply(category_maps, function(category) {
tm_shape(jakarta_district_counts) +
tm_polygons(
col = category,
palette = "Blues",
title = paste(category),
breaks = common_breaks
) +
tm_layout(main.title = paste(category), main.title.size = 1.2)
})
tmap_arrange(map_list[[1]], map_list[[2]], map_list[[3]], map_list[[4]], ncol = 2, nrow = 2)
tmap_arrange(map_list[[5]], map_list[[6]], map_list[[7]], map_list[[8]], ncol = 2, nrow = 2)
We can also visualize it using the tooltip of each district’s centroid we have calculated earlier. We join the tables for jakarta_district_centroid and the POI mapping and append a tooltip column
colnames(jakarta_district_centroid)[1] "province" "city" "district" "geometry" "centroid"
[6] "centroid_lat" "centroid_lng"
unique(poi_category$category)[1] "Facilities_Services" "Essentials"
[3] "Offices_Business" "Cultural_Attractions"
[5] "Restaurants_Food" "Recreation_Entertainment"
[7] "Others" "Shops"
[9] "Tourism_Spots"
- Ensure
districtcolumns are in lowercase in both dataframes
poi_aggregated <- poi_aggregated %>%
mutate(district = tolower(district))
jakarta_district_centroid <- jakarta_district_centroid %>%
mutate(district = tolower(district))- Pivot
poi_aggregatedto create a separate column for each category
tooltip_data <- poi_aggregated %>%
pivot_wider(
names_from = category,
values_from = count,
values_fill = list(count = 0) # Fill missing values with 0
)- Ensure the columns are within tooltip_data, for values with NA (categories that are not present for that district), we will with 0
required_categories <- c(
"Facilities_Services", "Restaurants_Food", "Essentials",
"Offices_Business", "Cultural_Attractions", "Recreation_Entertainment",
"Shops", "Tourism_Spots", "Others"
)
for (category in required_categories) {
if (!category %in% colnames(tooltip_data)) {
tooltip_data[[category]] <- 0
}
}
jakarta_district_centroid_expanded <- jakarta_district_centroid %>%
left_join(tooltip_data, by = "district")- We plot the map
tmap_mode("view")tmap mode set to interactive viewing
tm_shape(jakarta_district) +
tm_polygons(alpha = 0.3, border.col = "black") + # District boundaries
tm_shape(jakarta_district_centroid_expanded) +
tm_dots(
size = 0.1, col = "blue", alpha = 0.5,
popup.vars = c(
"District" = "district",
"Facilities_Services" = "Facilities_Services",
"Restaurants_Food" = "Restaurants_Food",
"Essentials" = "Essentials",
"Offices_Business" = "Offices_Business",
"Cultural_Attractions" = "Cultural_Attractions",
"Recreation_Entertainment" = "Recreation_Entertainment",
"Shops" = "Shops",
"Tourism_Spots" = "Tourism_Spots",
"Others" = "Others"
)
) +
tm_basemap("OpenStreetMap")tmap_mode("plot")tmap mode set to plotting
Now that we can see the number of POI categories per district by clicking into the centroid, we can also visualize this distribution through exploring the top 10 districts with the most POI to potentially use for subsequent analysis:
- We obtain the top_districts by summing up the count across all categories per district
- We filter to the top 5 districts
- Arrange the Category count by descending order per district (using )
- Create a Grouped Bar Chart
top_districts <- poi_aggregated %>%
group_by(district) %>%
summarise(total_count = sum(count)) %>%
arrange(desc(total_count)) %>%
slice(1:5) %>%
pull(district)
poi_aggregated_top <- poi_aggregated %>%
filter(district %in% top_districts)
spacing_rows <- poi_aggregated_top %>%
distinct(district) %>%
mutate(category = NA, count = NA)
poi_aggregated_with_spacing <- bind_rows(poi_aggregated_top, spacing_rows) %>%
arrange(district)#create a grouped bar chart
ggplot(poi_aggregated_with_spacing, aes(y = district, x = count, fill = category)) +
geom_bar(stat = "identity", position = "dodge", na.rm = TRUE) +
labs(
title = "Distribution of POI Categories by District",
x = "Count",
y = "District",
fill = "Category"
) +
theme_minimal() +
theme(
axis.text.y = element_text(angle = 0, hjust = 1),
legend.position = "bottom",
plot.margin = margin(10, 10, 10, 10)
) +
scale_fill_brewer(palette = "Set3")
5.6 Prototype Samples for Shiny App
After exploring the various different variables we can filter with, this would be our sample interface by filtering with the above variables for OD Analysis - Day of Week - Time Cluster - Weather Conditions - Vehicle Type.
With this as our base template:
6.0 Spatial Interaction Modelling
Aside from the graphical representation of the desire lines with respect to the selected variables, we will also explore the implementation of interaction models of OD data of the Grab Trips.
We will be exploring SIM to determine factors affecting Grab Vehicle demand flows during Fridays in the our Grab Dataset which spans 2 weeks in Jakarta.
We will be exploring
- Origin Constrained Model
- Destination Constrained Model
- Doubly Constrained Model
head(poi_aggregated)# A tibble: 6 × 3
district category count
<chr> <chr> <int>
1 cakung Cultural_Attractions 2
2 cakung Essentials 12
3 cakung Facilities_Services 22
4 cakung Offices_Business 18
5 cakung Others 1
6 cakung Restaurants_Food 3
6.1 Computing Distance Matrix
The computed distance matrix shows the distance between pairs of locations, in this case the distance between origin district and destination districts of each trajectory of the trips. The diagonal of the table will demonstrate that a location’s distance from itself (e.g origin district distance from origin distrct) will be 0.
We will use the jakarta_district_centroid_expanded to compute the distance of each district from each other
jakarta_dist_centroid_sp <- as(jakarta_district_centroid_expanded$centroid, "Spatial")
jakarta_dist_centroid_spclass : SpatialPoints
features : 44
extent : 13484362, 13585052, -3018643, -2900311 (xmin, xmax, ymin, ymax)
crs : +proj=tmerc +lat_0=0 +lon_0=30 +k=1 +x_0=300000 +y_0=0 +ellps=krass +units=m +no_defs
For the purposes of optimizing data processing for our final Shiny application, we will process using sp method as compared to sf, due to less compute power generally needed for sp functions.
We use spDists() of sp package to compute the distance between centroids of the Jakarta Districts
dist_districts <- spDists(jakarta_dist_centroid_sp, longlat=FALSE)
head(dist_districts, n=c(5,5)) [,1] [,2] [,3] [,4] [,5]
[1,] 0.00 33262.49 94446.12 81967.21 28291.84
[2,] 33262.49 0.00 61294.34 61065.20 42488.85
[3,] 94446.12 61294.34 0.00 68589.05 95302.01
[4,] 81967.21 61065.20 68589.05 0.00 101922.70
[5,] 28291.84 42488.85 95302.01 101922.70 0.00
For ease of visualization, we can use the kableExtra package to plot a highlight table for the distance matrix
Before which, we convert it into a data frame and label the rows and columns with district names
dist_districts_df <- as.data.frame(dist_districts)
rownames(dist_districts) <- jakarta_district_centroid_expanded$district
colnames(dist_districts) <- jakarta_district_centroid_expanded$districtAt this juncture, we represent the distance matrix calculated to display using the Highlight table of distances:
In our final prototype, we also consider cognitive load and visual comprehension so we will mock some changes we might implement, such as rounding to the nearest integer, using a sequential colour scale (where higher distance values represent longer distances as well as ease of reading the data)
- Round the Distance to the nearest integer
rounded_distances <- dist_districts_df[1:44, 1:44] %>%
mutate(across(everything(), round))- Define a sequential color palette (using RBrewer)
color_palette <- colorRampPalette(brewer.pal(9, "Blues"))(100)- Apply the color palette to the distance values. As values above 70k can become dark, we will conditionally make it into a white font
highlighted_distances <- rounded_distances %>%
mutate(across(everything(),
~ cell_spec(.,
color = ifelse(. > 90000, "white", "black"),
background = spec_color(as.numeric(.),
option = "custom",
scale_from = range(rounded_distances, na.rm = TRUE),
palette = color_palette),
bold = TRUE)))- We create the Highlight table
dist_matrix_highlight <- kable(highlighted_distances, escape = FALSE, format = "html") %>%
kable_styling("striped", full_width = TRUE) %>%
add_header_above(c(" " = 1, "Distance Matrix" = 43))
dist_matrix_highlight| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 | V11 | V12 | V13 | V14 | V15 | V16 | V17 | V18 | V19 | V20 | V21 | V22 | V23 | V24 | V25 | V26 | V27 | V28 | V29 | V30 | V31 | V32 | V33 | V34 | V35 | V36 | V37 | V38 | V39 | V40 | V41 | V42 | V43 | V44 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 33262 | 94446 | 81967 | 28292 | 64522 | 71799 | 41186 | 46710 | 21978 | 56386 | 71165 | 85196 | 33815 | 39344 | 110332 | 68270 | 78067 | 78093 | 21166 | 40388 | 90523 | 34965 | 52479 | 41945 | 61664 | 35340 | 48144 | 54441 | 66063 | 53535 | 68673 | 73567 | 85634 | 87349 | 23632 | 51485 | 43230 | 52353 | 58752 | 65331 | 60279 | 39679 | 44837 |
| 33262 | 0 | 61294 | 61065 | 42489 | 67665 | 69558 | 14199 | 17160 | 33560 | 23163 | 37989 | 72555 | 21660 | 6187 | 77380 | 41951 | 50687 | 45205 | 18635 | 9920 | 57913 | 32082 | 43320 | 40315 | 38883 | 10482 | 16443 | 27423 | 33357 | 35603 | 52897 | 66571 | 54358 | 58887 | 11079 | 20098 | 10233 | 24744 | 27118 | 33178 | 28989 | 22603 | 23202 |
| 94446 | 61294 | 0 | 68589 | 95302 | 109158 | 104370 | 55554 | 49643 | 91374 | 38144 | 23305 | 90985 | 72626 | 55419 | 16643 | 51240 | 48269 | 22466 | 76241 | 54588 | 21028 | 78107 | 81172 | 87355 | 59384 | 62745 | 49051 | 47031 | 32294 | 67487 | 75538 | 96151 | 20645 | 45601 | 71876 | 44048 | 51852 | 52305 | 37269 | 30531 | 40980 | 63767 | 62486 |
| 81967 | 61065 | 68589 | 0 | 101923 | 52073 | 42693 | 70717 | 68651 | 63941 | 56096 | 58637 | 22807 | 48268 | 57361 | 82192 | 22018 | 20443 | 47983 | 78175 | 65452 | 48598 | 93131 | 33577 | 45999 | 22185 | 51750 | 47868 | 76817 | 45737 | 28445 | 16326 | 32663 | 80022 | 24670 | 62653 | 65557 | 54665 | 37047 | 66467 | 66034 | 40162 | 81983 | 38946 |
| 28292 | 42489 | 95302 | 101923 | 0 | 92357 | 98806 | 40571 | 46237 | 49397 | 60903 | 73727 | 109298 | 55043 | 47940 | 109216 | 84218 | 93154 | 84638 | 24028 | 43390 | 97610 | 18025 | 76833 | 68013 | 80001 | 50378 | 58713 | 48779 | 73805 | 74230 | 91027 | 99382 | 81082 | 101343 | 39341 | 52004 | 51834 | 66998 | 58186 | 64820 | 71057 | 31603 | 62977 |
| 64522 | 67665 | 109158 | 52073 | 92357 | 0 | 12031 | 81827 | 83690 | 42970 | 79964 | 90718 | 37017 | 46030 | 68824 | 125149 | 58997 | 65677 | 86716 | 74980 | 77036 | 93191 | 93129 | 28247 | 27383 | 50022 | 58114 | 66548 | 94176 | 77968 | 41821 | 37138 | 21178 | 112833 | 73858 | 60456 | 84279 | 69277 | 58512 | 89326 | 92784 | 69203 | 89093 | 49975 |
| 71799 | 69558 | 104370 | 42693 | 98806 | 12031 | 0 | 83384 | 84422 | 49827 | 78503 | 87715 | 25368 | 48175 | 69650 | 119848 | 53240 | 58564 | 81960 | 79501 | 78274 | 87007 | 97553 | 26720 | 31087 | 45104 | 59368 | 65541 | 94737 | 74472 | 38829 | 29504 | 10174 | 110152 | 65923 | 63969 | 84187 | 69397 | 56186 | 88460 | 91116 | 65987 | 91820 | 49166 |
| 41186 | 14199 | 55554 | 70717 | 40571 | 81827 | 83384 | 0 | 5919 | 46632 | 20574 | 33261 | 84596 | 35799 | 14182 | 70515 | 49784 | 57102 | 44243 | 21053 | 5402 | 57180 | 25307 | 56888 | 54500 | 48996 | 24017 | 22888 | 13961 | 34044 | 47796 | 64729 | 79863 | 44532 | 63929 | 23685 | 11568 | 16102 | 33713 | 18379 | 25167 | 33315 | 11624 | 35487 |
| 46710 | 17160 | 49643 | 68651 | 46237 | 83690 | 84422 | 5919 | 0 | 50596 | 15650 | 27495 | 83848 | 37859 | 14877 | 64610 | 47172 | 53795 | 38978 | 26905 | 7270 | 51840 | 30335 | 57729 | 56598 | 47472 | 25591 | 21035 | 10501 | 29390 | 47540 | 63989 | 80226 | 38924 | 60048 | 27729 | 5768 | 15268 | 32115 | 12465 | 19251 | 29854 | 15967 | 35608 |
| 21978 | 33560 | 91374 | 63941 | 49397 | 42970 | 49827 | 46632 | 50596 | 0 | 54318 | 68662 | 64179 | 19806 | 37840 | 107956 | 54456 | 64308 | 71632 | 33656 | 43330 | 82693 | 51405 | 31960 | 20403 | 46187 | 28729 | 42323 | 60513 | 59825 | 36481 | 49193 | 51807 | 87367 | 73985 | 22960 | 53565 | 40508 | 41691 | 60371 | 65966 | 52100 | 50451 | 31842 |
| 56386 | 23163 | 38144 | 56096 | 60903 | 79964 | 78503 | 20574 | 15650 | 54318 | 0 | 14843 | 73712 | 37120 | 17294 | 54345 | 34097 | 39364 | 23737 | 39294 | 17675 | 36707 | 45810 | 52182 | 54482 | 36699 | 25697 | 13517 | 21089 | 13766 | 39801 | 54324 | 72720 | 33741 | 44844 | 33779 | 10580 | 13920 | 22511 | 10544 | 12833 | 15961 | 31595 | 29995 |
| 71165 | 37989 | 23305 | 58637 | 73727 | 90718 | 87715 | 33261 | 27495 | 68662 | 14843 | 0 | 78958 | 50583 | 32131 | 39545 | 37484 | 39099 | 14216 | 53399 | 31625 | 25700 | 57379 | 62485 | 66752 | 43230 | 39933 | 26589 | 27838 | 13493 | 49125 | 60876 | 80734 | 22482 | 41409 | 48607 | 21731 | 28640 | 32240 | 16026 | 10603 | 21729 | 42814 | 41657 |
| 85196 | 72555 | 90985 | 22807 | 109298 | 37017 | 25368 | 84596 | 83848 | 64179 | 73712 | 78958 | 0 | 54400 | 70459 | 104925 | 41491 | 42729 | 69705 | 87002 | 79194 | 71318 | 103904 | 32762 | 43823 | 37019 | 62190 | 62941 | 93190 | 65562 | 36973 | 19879 | 15968 | 101033 | 47438 | 70716 | 81897 | 68765 | 51962 | 84253 | 85039 | 58510 | 94947 | 49404 |
| 33815 | 21660 | 72626 | 48268 | 55043 | 46030 | 48175 | 35799 | 37859 | 19806 | 37120 | 50583 | 54400 | 0 | 23133 | 89268 | 35379 | 45290 | 52169 | 32717 | 31045 | 62952 | 50185 | 22725 | 18758 | 27994 | 12357 | 24019 | 48359 | 40594 | 19838 | 36035 | 46111 | 70847 | 54870 | 16465 | 39107 | 24356 | 21898 | 44994 | 49629 | 32529 | 43651 | 12138 |
| 39344 | 6187 | 55419 | 57361 | 47940 | 68824 | 69650 | 14182 | 14877 | 37840 | 17294 | 32131 | 70459 | 23133 | 0 | 71633 | 37306 | 45600 | 39018 | 24408 | 8815 | 51734 | 36134 | 43010 | 41808 | 35339 | 10785 | 10790 | 25353 | 27183 | 33617 | 50609 | 65791 | 49537 | 53459 | 16489 | 16134 | 4074 | 20403 | 22593 | 28156 | 23162 | 24756 | 21313 |
| 110332 | 77380 | 16643 | 82192 | 109216 | 125149 | 119848 | 70515 | 64610 | 107956 | 54345 | 39545 | 104925 | 89268 | 71633 | 0 | 66612 | 62399 | 38550 | 91482 | 70120 | 33607 | 91560 | 97347 | 103877 | 75175 | 79286 | 65642 | 60443 | 48902 | 83680 | 90657 | 111280 | 29146 | 58038 | 88122 | 59245 | 68185 | 68860 | 52149 | 45375 | 57533 | 77642 | 79082 |
| 68270 | 41951 | 51240 | 22018 | 84218 | 58997 | 53240 | 49784 | 47172 | 54456 | 34097 | 37484 | 41491 | 35379 | 37306 | 66612 | 0 | 9911 | 29013 | 60191 | 44712 | 34196 | 73438 | 32362 | 41945 | 9492 | 34213 | 26991 | 54930 | 24172 | 19764 | 24342 | 44920 | 59569 | 19538 | 46166 | 43741 | 34083 | 17232 | 44449 | 44184 | 18148 | 61298 | 23521 |
| 78067 | 50687 | 48269 | 20443 | 93154 | 65677 | 58564 | 57102 | 53795 | 64308 | 39364 | 39099 | 42729 | 45290 | 45600 | 62399 | 9911 | 0 | 27614 | 69177 | 52310 | 28884 | 81516 | 40570 | 51025 | 18573 | 43633 | 34912 | 60446 | 27091 | 28967 | 29066 | 49381 | 59851 | 9697 | 55683 | 49676 | 42054 | 26348 | 49078 | 47494 | 23941 | 68722 | 33402 |
| 78093 | 45205 | 22466 | 47983 | 84638 | 86716 | 81960 | 44243 | 38978 | 71632 | 23737 | 14216 | 69705 | 52169 | 39018 | 38550 | 29013 | 27614 | 0 | 62644 | 41329 | 12976 | 69313 | 58809 | 65555 | 36927 | 43863 | 30182 | 41348 | 12084 | 45135 | 53354 | 73904 | 32387 | 28017 | 54574 | 33377 | 34998 | 30810 | 29163 | 24669 | 19664 | 54886 | 41163 |
| 21166 | 18635 | 76241 | 78175 | 24028 | 74980 | 79501 | 21053 | 26905 | 33656 | 39294 | 53399 | 87002 | 32717 | 24408 | 91482 | 60191 | 69177 | 62644 | 0 | 21781 | 75558 | 18154 | 55326 | 48472 | 56131 | 26460 | 35043 | 33559 | 51195 | 50965 | 68088 | 78562 | 65457 | 77503 | 16289 | 32261 | 28433 | 42974 | 39341 | 46108 | 47545 | 18608 | 39282 |
| 40388 | 9920 | 54588 | 65452 | 43390 | 77036 | 78274 | 5402 | 7270 | 43330 | 17675 | 31625 | 79194 | 31045 | 8815 | 70120 | 44712 | 52310 | 41329 | 21781 | 0 | 54303 | 29402 | 51702 | 49800 | 43653 | 18989 | 17731 | 17523 | 30448 | 42410 | 59327 | 74580 | 45522 | 59458 | 20493 | 11123 | 10788 | 28415 | 18393 | 24945 | 28751 | 16595 | 30127 |
| 90523 | 57913 | 21028 | 48598 | 97610 | 93191 | 87007 | 57180 | 51840 | 82693 | 36707 | 25700 | 71318 | 62952 | 51734 | 33607 | 34196 | 28884 | 12976 | 75558 | 54303 | 0 | 82166 | 66222 | 74488 | 43377 | 55769 | 42394 | 53505 | 24560 | 52897 | 57575 | 78105 | 38044 | 24781 | 66914 | 46182 | 47681 | 41087 | 41489 | 36269 | 30776 | 67687 | 51307 |
| 34965 | 32082 | 78107 | 93131 | 18025 | 93129 | 97553 | 25307 | 30335 | 51405 | 45810 | 57379 | 103904 | 50185 | 36134 | 91560 | 73438 | 81516 | 69313 | 18154 | 29402 | 82166 | 0 | 72910 | 66576 | 70955 | 41951 | 46628 | 31182 | 59350 | 67228 | 84542 | 96272 | 63183 | 88853 | 33789 | 35990 | 39478 | 56464 | 41468 | 47875 | 58145 | 14566 | 55007 |
| 52479 | 43320 | 81172 | 33577 | 76833 | 28247 | 26720 | 56888 | 57729 | 31960 | 52182 | 62485 | 32762 | 22725 | 43010 | 97347 | 32362 | 40570 | 58809 | 55326 | 51702 | 66222 | 72910 | 0 | 12504 | 22923 | 32948 | 38992 | 68022 | 49737 | 13685 | 17699 | 23413 | 84669 | 49781 | 39156 | 57486 | 42677 | 30325 | 61928 | 64930 | 40960 | 65836 | 22493 |
| 41945 | 40315 | 87355 | 45999 | 68013 | 27383 | 31087 | 54500 | 56598 | 20403 | 54482 | 66752 | 43823 | 18758 | 41808 | 103877 | 41945 | 51025 | 65555 | 48472 | 49800 | 74488 | 66576 | 12504 | 0 | 32556 | 31028 | 40980 | 67100 | 55127 | 22182 | 30201 | 31642 | 88074 | 60555 | 33289 | 57608 | 42669 | 35062 | 63146 | 67260 | 46377 | 61767 | 25109 |
| 61664 | 38883 | 59384 | 22185 | 80001 | 50022 | 45104 | 48996 | 47472 | 46187 | 36699 | 43230 | 37019 | 27994 | 35339 | 75175 | 9492 | 18573 | 36927 | 56131 | 43653 | 43377 | 70955 | 22923 | 32556 | 0 | 29671 | 26440 | 56396 | 29768 | 10409 | 17809 | 37555 | 65694 | 28225 | 40938 | 45065 | 32895 | 15368 | 47237 | 48275 | 21861 | 60064 | 17206 |
| 35340 | 10482 | 62745 | 51750 | 50378 | 58114 | 59368 | 24017 | 25591 | 28729 | 25697 | 39933 | 62190 | 12357 | 10785 | 79286 | 34213 | 43633 | 43863 | 26460 | 18989 | 55769 | 41951 | 32948 | 31028 | 29671 | 0 | 13815 | 36089 | 31814 | 25316 | 42640 | 56124 | 59170 | 52544 | 12095 | 26771 | 12232 | 17347 | 32828 | 37801 | 25122 | 33084 | 13068 |
| 48144 | 16443 | 49051 | 47868 | 58713 | 66548 | 65541 | 22888 | 21035 | 42323 | 13517 | 26589 | 62941 | 24019 | 10790 | 65642 | 26991 | 34912 | 30182 | 35043 | 17731 | 42394 | 46628 | 38992 | 40980 | 26440 | 13815 | 0 | 30298 | 18100 | 27292 | 43134 | 60334 | 47134 | 42670 | 24522 | 19110 | 7150 | 11080 | 22937 | 26337 | 12562 | 34326 | 16592 |
| 54441 | 27423 | 47031 | 76817 | 48779 | 94176 | 94737 | 13961 | 10501 | 60513 | 21089 | 27838 | 93190 | 48359 | 25353 | 60443 | 54930 | 60446 | 41348 | 33559 | 17523 | 53505 | 31182 | 68022 | 67100 | 56396 | 36089 | 30298 | 0 | 34142 | 57387 | 73420 | 90278 | 32595 | 65580 | 37553 | 11336 | 25364 | 41229 | 12221 | 17469 | 36918 | 17278 | 45729 |
| 66063 | 33357 | 32294 | 45737 | 73805 | 77968 | 74472 | 34044 | 29390 | 59825 | 13766 | 13493 | 65562 | 40594 | 27183 | 48902 | 24172 | 27091 | 12084 | 51195 | 30448 | 24560 | 59350 | 49737 | 55127 | 29768 | 31814 | 18100 | 34142 | 0 | 36174 | 47383 | 67306 | 35927 | 31443 | 42511 | 24296 | 23124 | 20069 | 22275 | 20451 | 8826 | 45288 | 30192 |
| 53535 | 35603 | 67487 | 28445 | 74230 | 41821 | 38829 | 47796 | 47540 | 36481 | 39801 | 49125 | 36973 | 19838 | 33617 | 83680 | 19764 | 28967 | 45135 | 50965 | 42410 | 52897 | 67228 | 13685 | 22182 | 10409 | 25316 | 27292 | 57387 | 36174 | 0 | 17324 | 33050 | 71463 | 38592 | 34921 | 46354 | 32283 | 17368 | 49970 | 52317 | 27460 | 57980 | 12431 |
| 68673 | 52897 | 75538 | 16326 | 91027 | 37138 | 29504 | 64729 | 63989 | 49193 | 54324 | 60876 | 19879 | 36035 | 50609 | 90657 | 24342 | 29066 | 53354 | 68088 | 59327 | 57575 | 84542 | 17699 | 30201 | 17809 | 42640 | 43134 | 73420 | 47383 | 17324 | 0 | 20633 | 83281 | 36772 | 51903 | 62161 | 48887 | 32234 | 64826 | 66071 | 39665 | 75171 | 29697 |
| 73567 | 66571 | 96151 | 32663 | 99382 | 21178 | 10174 | 79863 | 80226 | 51807 | 72720 | 80734 | 15968 | 46111 | 65791 | 111280 | 44920 | 49381 | 73904 | 78562 | 74580 | 78105 | 96272 | 23413 | 31642 | 37555 | 56124 | 60334 | 90278 | 67306 | 33050 | 20633 | 0 | 103214 | 56298 | 62489 | 79353 | 64981 | 50209 | 82987 | 85029 | 59142 | 89147 | 44619 |
| 85634 | 54358 | 20645 | 80022 | 81082 | 112833 | 110152 | 44532 | 38924 | 87367 | 33741 | 22482 | 101033 | 70847 | 49537 | 29146 | 59569 | 59851 | 32387 | 65457 | 45522 | 38044 | 63183 | 84669 | 88074 | 65694 | 59170 | 47134 | 32595 | 35927 | 71463 | 83281 | 103214 | 0 | 60079 | 65436 | 34450 | 47002 | 54344 | 27262 | 21411 | 44176 | 49831 | 63213 |
| 87349 | 58887 | 45601 | 24670 | 101343 | 73858 | 65923 | 63929 | 60048 | 73985 | 44844 | 41409 | 47438 | 54870 | 53459 | 58038 | 19538 | 9697 | 28017 | 77503 | 59458 | 24781 | 88853 | 49781 | 60555 | 28225 | 52544 | 42670 | 65580 | 31443 | 38592 | 36772 | 56298 | 60079 | 0 | 64637 | 55407 | 49694 | 35200 | 53676 | 50962 | 30709 | 75516 | 42894 |
| 23632 | 11079 | 71876 | 62653 | 39341 | 60456 | 63969 | 23685 | 27729 | 22960 | 33779 | 48607 | 70716 | 16465 | 16489 | 88122 | 46166 | 55683 | 54574 | 16289 | 20493 | 66914 | 33789 | 39156 | 33289 | 40938 | 12095 | 24522 | 37553 | 42511 | 34921 | 51903 | 62489 | 65436 | 64637 | 0 | 31124 | 20063 | 29438 | 38190 | 44241 | 36677 | 28779 | 23765 |
| 51485 | 20098 | 44048 | 65557 | 52004 | 84279 | 84187 | 11568 | 5768 | 53565 | 10580 | 21731 | 81897 | 39107 | 16134 | 59245 | 43741 | 49676 | 33377 | 32261 | 11123 | 46182 | 35990 | 57486 | 57608 | 45065 | 26771 | 19110 | 11336 | 24296 | 46354 | 62161 | 79353 | 34450 | 55407 | 31124 | 0 | 15003 | 29939 | 7272 | 13908 | 25883 | 21512 | 35026 |
| 43230 | 10233 | 51852 | 54665 | 51834 | 69277 | 69397 | 16102 | 15268 | 40508 | 13920 | 28640 | 68765 | 24356 | 4074 | 68185 | 34083 | 42054 | 34998 | 28433 | 10788 | 47681 | 39478 | 42677 | 42669 | 32895 | 12232 | 7150 | 25364 | 23124 | 32283 | 48887 | 64981 | 47002 | 49694 | 20063 | 15003 | 0 | 17628 | 20643 | 25604 | 19223 | 27343 | 20380 |
| 52353 | 24744 | 52305 | 37047 | 66998 | 58512 | 56186 | 33713 | 32115 | 41691 | 22511 | 32240 | 51962 | 21898 | 20403 | 68860 | 17232 | 26348 | 30810 | 42974 | 28415 | 41087 | 56464 | 30325 | 35062 | 15368 | 17347 | 11080 | 41229 | 20069 | 17368 | 32234 | 50209 | 54344 | 35200 | 29438 | 29939 | 17628 | 0 | 32839 | 34949 | 11328 | 44937 | 10359 |
| 58752 | 27118 | 37269 | 66467 | 58186 | 89326 | 88460 | 18379 | 12465 | 60371 | 10544 | 16026 | 84253 | 44994 | 22593 | 52149 | 44449 | 49078 | 29163 | 39341 | 18393 | 41489 | 41468 | 61928 | 63146 | 47237 | 32828 | 22937 | 12221 | 22275 | 49970 | 64826 | 82987 | 27262 | 53676 | 38190 | 7272 | 20643 | 32839 | 0 | 6788 | 26312 | 26919 | 39486 |
| 65331 | 33178 | 30531 | 66034 | 64820 | 92784 | 91116 | 25167 | 19251 | 65966 | 12833 | 10603 | 85039 | 49629 | 28156 | 45375 | 44184 | 47494 | 24669 | 46108 | 24945 | 36269 | 47875 | 64930 | 67260 | 48275 | 37801 | 26337 | 17469 | 20451 | 52317 | 66071 | 85029 | 21411 | 50962 | 44241 | 13908 | 25604 | 34949 | 6788 | 0 | 26530 | 33392 | 42822 |
| 60279 | 28989 | 40980 | 40162 | 71057 | 69203 | 65987 | 33315 | 29854 | 52100 | 15961 | 21729 | 58510 | 32529 | 23162 | 57533 | 18148 | 23941 | 19664 | 47545 | 28751 | 30776 | 58145 | 40960 | 46377 | 21861 | 25122 | 12562 | 36918 | 8826 | 27460 | 39665 | 59142 | 44176 | 30709 | 36677 | 25883 | 19223 | 11328 | 26312 | 26530 | 0 | 44933 | 21596 |
| 39679 | 22603 | 63767 | 81983 | 31603 | 89093 | 91820 | 11624 | 15967 | 50451 | 31595 | 42814 | 94947 | 43651 | 24756 | 77642 | 61298 | 68722 | 54886 | 18608 | 16595 | 67687 | 14566 | 65836 | 61767 | 60064 | 33084 | 34326 | 17278 | 45288 | 57980 | 75171 | 89147 | 49831 | 75516 | 28779 | 21512 | 27343 | 44937 | 26919 | 33392 | 44933 | 0 | 45551 |
| 44837 | 23202 | 62486 | 38946 | 62977 | 49975 | 49166 | 35487 | 35608 | 31842 | 29995 | 41657 | 49404 | 12138 | 21313 | 79082 | 23521 | 33402 | 41163 | 39282 | 30127 | 51307 | 55007 | 22493 | 25109 | 17206 | 13068 | 16592 | 45729 | 30192 | 12431 | 29697 | 44619 | 63213 | 42894 | 23765 | 35026 | 20380 | 10359 | 39486 | 42822 | 21596 | 45551 | 0 |
colnames(jakarta_district_centroid_expanded) [1] "province" "city"
[3] "district" "centroid_lat"
[5] "centroid_lng" "Cultural_Attractions"
[7] "Essentials" "Facilities_Services"
[9] "Offices_Business" "Others"
[11] "Restaurants_Food" "Shops"
[13] "Recreation_Entertainment" "Tourism_Spots"
[15] "geometry" "centroid"
6.1.1 Pivot Distance Value by Origin and Destination District
distPair <- melt(dist_districts) %>%
rename(dist = value)
head(distPair, 10) Var1 Var2 dist
1 cakung cakung 0.00
2 cempaka putih cakung 33262.49
3 cengkareng cakung 94446.12
4 cilandak cakung 81967.21
5 cilincing cakung 28291.84
6 cipayung cakung 64521.55
7 ciracas cakung 71798.66
8 danau sunter cakung 41185.87
9 danau sunter dll cakung 46710.36
10 duren sawit cakung 21978.34
There are intrazonal distances with 0 values (interzones)
Before proceeding, we update them with the lowest non-zero distance values before applying log transformation given that log 0 is an undefined value
distPair %>%
filter(dist > 0) %>%
summary() Var1 Var2 dist
cakung : 43 cakung : 43 Min. : 4074
cempaka putih: 43 cempaka putih: 43 1st Qu.: 28445
cengkareng : 43 cengkareng : 43 Median : 44576
cilandak : 43 cilandak : 43 Mean : 46934
cilincing : 43 cilincing : 43 3rd Qu.: 62941
cipayung : 43 cipayung : 43 Max. :125149
(Other) :1634 (Other) :1634
The lowest minimum distance is 4074 m, any distance less than this can represent the intrazonal distance. For consistency and ease of remberance, we stick to 2000m as the intra-zonal distance
distPair$dist <- ifelse (distPair$dist == 0,
2000, distPair$dist)
distPair <- distPair %>%
rename(origin=Var1,
destination =Var2) %>%
mutate(across(c(origin, destination), as.factor))
summary(distPair) origin destination dist
cakung : 44 cakung : 44 Min. : 2000
cempaka putih: 44 cempaka putih: 44 1st Qu.: 27330
cengkareng : 44 cengkareng : 44 Median : 43652
cilandak : 44 cilandak : 44 Mean : 45912
cilincing : 44 cilincing : 44 3rd Qu.: 62485
cipayung : 44 cipayung : 44 Max. :125149
(Other) :1672 (Other) :1672
6.2 Applying Log Transformation to Variables of Interest
We apply log transformation for the variables of interest (categories of POI for push and pull factors), to use Poisson Regresion method.
- For the subset of POIs in Origin and Destination Districts, we rename accordingly
poi_aggregated_origin <- poi_aggregated %>%
rename(origin = district)
# Rename district to 'destination' for merging with destination data
poi_aggregated_destination <- poi_aggregated %>%
rename(destination = district)- We merge the distPair with POI data for both origin and destination subsets
- We Pivot the POI data wide to separate the categories into individual columns & counts
dist_with_POI <- distPair %>%
left_join(poi_aggregated_origin, by = "origin") %>%
left_join(poi_aggregated_destination, by = "destination", suffix = c("_origin", "_destination"))
poi_origin_wide <- dist_with_POI %>%
pivot_wider(
names_from = category_origin,
values_from = count_origin,
names_prefix = "origin_",
values_fill = list(count_origin = 0)
)
poi_orig_dest_wide <- poi_origin_wide %>%
pivot_wider(
names_from = category_destination,
values_from = count_destination,
names_prefix = "destination_",
values_fill = list(count_destination = 0)
)- We apply Log Transformation to the category columns
columns_to_log_transform <- colnames(poi_orig_dest_wide)[4:19]
poi_orig_dest_wide[columns_to_log_transform] <- lapply(
poi_orig_dest_wide[columns_to_log_transform],
function(x) log(as.numeric(as.character(x)) + 1)
)
head(poi_orig_dest_wide[columns_to_log_transform])# A tibble: 6 × 16
origin_Cultural_Attractions origin_Essentials origin_Facilities_Services
<dbl> <dbl> <dbl>
1 1.10 2.56 3.14
2 0.693 2.20 2.56
3 2.30 3.95 4.23
4 1.61 3.26 4.04
5 1.95 3.43 3.71
6 1.10 1.61 3.00
# ℹ 13 more variables: origin_Offices_Business <dbl>, origin_Others <dbl>,
# origin_Restaurants_Food <dbl>, origin_Shops <dbl>,
# origin_Recreation_Entertainment <dbl>,
# destination_Cultural_Attractions <dbl>, destination_Essentials <dbl>,
# destination_Facilities_Services <dbl>, destination_Offices_Business <dbl>,
# destination_Others <dbl>, destination_Restaurants_Food <dbl>,
# destination_Shops <dbl>, destination_Recreation_Entertainment <dbl>
6.3 Origin-Constrained Spatial Interaction Model
The origin-constrained model focuses on constraints at the origin locations, meaning the sum of interactions originating from each origin equals a known total, such as the total number of trips from each origin district. Here, only pull (attractive) factors of the destination districts will be used.
Before doing so, we merge od_data and the newly created poi_orig_dest_wide
od_data <- od_data %>%
rename(
origin = origin_district,
destination = destination_district
)
tripsData <- left_join(od_data, poi_orig_dest_wide, by = c("origin", "destination"))
head(tripsData)# A tibble: 6 × 27
origin destination origin_time_cluster origin_weather_descr…¹ trj_id
<chr> <chr> <chr> <chr> <chr>
1 kelapa gading pasar rebo morning lull not_rain 10003
2 setia budi pasar ming… morning peak rain 10043
3 makasar duren sawit morning peak rain 10061
4 setia budi mampang pr… morning peak not_rain 10072
5 kebayoran lama pasar ming… morning lull not_rain 10086
6 grogol petambur… tanah abang midnight lull not_rain 10089
# ℹ abbreviated name: ¹origin_weather_description_category
# ℹ 22 more variables: driving_mode <chr>, centroid_lat_origin <dbl>,
# centroid_lng_origin <dbl>, centroid_lat_destination <dbl>,
# centroid_lng_destination <dbl>, dist <dbl>,
# origin_Cultural_Attractions <dbl>, origin_Essentials <dbl>,
# origin_Facilities_Services <dbl>, origin_Offices_Business <dbl>,
# origin_Others <dbl>, origin_Restaurants_Food <dbl>, origin_Shops <dbl>, …
tripsData_counts <- tripsData %>%
group_by(origin, destination) %>%
summarise(trips_count = n_distinct(trj_id), .groups = 'drop') # Count unique trips
tripsData <- left_join(tripsData, tripsData_counts, by = c("origin", "destination"))*For better viewing of variables in the 3 SIMs, we rename the origin and destination variables to read as such : “origin_district” or “deestination_district”
tripsData <- tripsData %>%
rename(
origin_ = origin,
destination_ = destination
)origSIM <- glm(
trips_count ~ origin_ + destination_Cultural_Attractions + destination_Essentials +
destination_Facilities_Services + destination_Offices_Business +
destination_Others + destination_Restaurants_Food +
destination_Shops + destination_Recreation_Entertainment +
dist - 1, # No intercept
family = poisson(link = "log"),
data = tripsData,
na.action = na.exclude
)
summary(origSIM)
Call:
glm(formula = trips_count ~ origin_ + destination_Cultural_Attractions +
destination_Essentials + destination_Facilities_Services +
destination_Offices_Business + destination_Others + destination_Restaurants_Food +
destination_Shops + destination_Recreation_Entertainment +
dist - 1, family = poisson(link = "log"), data = tripsData,
na.action = na.exclude)
Coefficients:
Estimate Std. Error z value Pr(>|z|)
origin_cakung 1.757e+00 9.602e-02 18.299 < 2e-16 ***
origin_cempaka putih 1.151e+00 8.098e-02 14.215 < 2e-16 ***
origin_cengkareng 2.000e+00 6.744e-02 29.656 < 2e-16 ***
origin_cilandak 1.824e+00 7.585e-02 24.048 < 2e-16 ***
origin_cilincing 1.882e+00 1.104e-01 17.047 < 2e-16 ***
origin_cipayung 2.171e+00 8.077e-02 26.883 < 2e-16 ***
origin_ciracas 2.030e+00 7.427e-02 27.330 < 2e-16 ***
origin_danau sunter dll 8.483e-01 3.812e-01 2.225 0.02608 *
origin_duren sawit 2.455e+00 6.651e-02 36.915 < 2e-16 ***
origin_gambir 1.463e+00 6.598e-02 22.168 < 2e-16 ***
origin_grogol petamburan 2.258e+00 5.847e-02 38.611 < 2e-16 ***
origin_jagakarsa 2.312e+00 7.264e-02 31.827 < 2e-16 ***
origin_jatinegara 1.918e+00 6.603e-02 29.041 < 2e-16 ***
origin_johar baru 7.807e-01 1.168e-01 6.685 2.31e-11 ***
origin_kali deres 1.832e+00 9.023e-02 20.305 < 2e-16 ***
origin_kebayoran baru 2.257e+00 5.870e-02 38.456 < 2e-16 ***
origin_kebayoran lama 2.055e+00 6.228e-02 32.996 < 2e-16 ***
origin_kebon jeruk 2.072e+00 6.038e-02 34.309 < 2e-16 ***
origin_kelapa gading 2.236e+00 6.263e-02 35.696 < 2e-16 ***
origin_kemayoran 1.319e+00 7.927e-02 16.640 < 2e-16 ***
origin_kembangan 2.002e+00 6.569e-02 30.479 < 2e-16 ***
origin_koja 1.533e+00 9.134e-02 16.778 < 2e-16 ***
origin_kramat jati 1.777e+00 7.280e-02 24.411 < 2e-16 ***
origin_makasar 1.471e+00 1.023e-01 14.372 < 2e-16 ***
origin_mampang prapatan 1.117e+00 8.251e-02 13.535 < 2e-16 ***
origin_matraman 9.069e-01 1.015e-01 8.931 < 2e-16 ***
origin_menteng 1.415e+00 6.699e-02 21.116 < 2e-16 ***
origin_pademangan 1.701e+00 7.487e-02 22.720 < 2e-16 ***
origin_palmerah 1.292e+00 7.098e-02 18.204 < 2e-16 ***
origin_pancoran 1.490e+00 7.305e-02 20.402 < 2e-16 ***
origin_pasar minggu 2.458e+00 6.085e-02 40.394 < 2e-16 ***
origin_pasar rebo 1.225e+00 1.114e-01 10.996 < 2e-16 ***
origin_penjaringan 2.339e+00 6.396e-02 36.562 < 2e-16 ***
origin_pesanggrahan 1.184e+00 9.893e-02 11.965 < 2e-16 ***
origin_pulo gadung 1.696e+00 7.214e-02 23.506 < 2e-16 ***
origin_sawah besar 1.216e+00 7.618e-02 15.961 < 2e-16 ***
origin_senen 1.330e+00 7.272e-02 18.288 < 2e-16 ***
origin_setia budi 2.202e+00 5.860e-02 37.582 < 2e-16 ***
origin_taman sari 1.508e+00 7.130e-02 21.148 < 2e-16 ***
origin_tambora 1.386e+00 7.208e-02 19.223 < 2e-16 ***
origin_tanah abang 2.185e+00 5.996e-02 36.440 < 2e-16 ***
origin_tanjung priok 2.142e+00 6.210e-02 34.482 < 2e-16 ***
origin_tebet 2.022e+00 6.135e-02 32.952 < 2e-16 ***
destination_Cultural_Attractions 3.063e-02 1.501e-02 2.041 0.04120 *
destination_Essentials 2.289e-01 1.813e-02 12.625 < 2e-16 ***
destination_Facilities_Services 6.091e-03 2.009e-02 0.303 0.76171
destination_Offices_Business -1.335e-02 7.795e-03 -1.713 0.08669 .
destination_Others 3.566e-02 1.249e-02 2.854 0.00431 **
destination_Restaurants_Food 9.069e-02 1.280e-02 7.084 1.40e-12 ***
destination_Shops 9.246e-02 2.175e-02 4.252 2.12e-05 ***
destination_Recreation_Entertainment 9.041e-02 1.106e-02 8.173 3.02e-16 ***
dist -4.832e-05 6.542e-07 -73.851 < 2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
(Dispersion parameter for poisson family taken to be 1)
Null deviance: 107406.8 on 3759 degrees of freedom
Residual deviance: 5523.5 on 3707 degrees of freedom
AIC: 19515
Number of Fisher Scoring iterations: 5
6.4 Destination-Constrained Spatial Interaction Model
In this model, we focus on the characteristics of origins that make them less attractive or less likely to generate demand for trips to destinations. Unlike the origin-constrained model, which emphasizes limitations or capacities at the origin, the destination-constrained model highlights the demand (attractiveness) of destinations by analyzing factors that make certain origins less likely to send trips.
destSIM <- glm(
trips_count ~ destination_ + origin_Cultural_Attractions + origin_Essentials +
origin_Facilities_Services + origin_Offices_Business +
origin_Others + origin_Restaurants_Food +
origin_Shops + origin_Recreation_Entertainment +
dist - 1, # No intercept
family = poisson(link = "log"),
data = tripsData,
na.action = na.exclude
)
summary(destSIM)
Call:
glm(formula = trips_count ~ destination_ + origin_Cultural_Attractions +
origin_Essentials + origin_Facilities_Services + origin_Offices_Business +
origin_Others + origin_Restaurants_Food + origin_Shops +
origin_Recreation_Entertainment + dist - 1, family = poisson(link = "log"),
data = tripsData, na.action = na.exclude)
Coefficients:
Estimate Std. Error z value Pr(>|z|)
destination_cakung 2.360e+00 6.860e-02 34.397 < 2e-16 ***
destination_cempaka putih 1.357e+00 8.843e-02 15.350 < 2e-16 ***
destination_cengkareng 2.207e+00 6.253e-02 35.295 < 2e-16 ***
destination_cilandak 2.290e+00 6.641e-02 34.489 < 2e-16 ***
destination_cilincing 1.943e+00 9.876e-02 19.670 < 2e-16 ***
destination_cipayung 2.039e+00 9.427e-02 21.632 < 2e-16 ***
destination_ciracas 2.001e+00 8.865e-02 22.567 < 2e-16 ***
destination_danau sunter 4.959e-01 5.023e-01 0.987 0.323475
destination_danau sunter dll -7.051e-01 7.092e-01 -0.994 0.320111
destination_duren sawit 2.339e+00 7.313e-02 31.979 < 2e-16 ***
destination_gambir 1.786e+00 5.935e-02 30.099 < 2e-16 ***
destination_grogol petamburan 2.003e+00 5.969e-02 33.561 < 2e-16 ***
destination_jagakarsa 2.755e+00 6.507e-02 42.342 < 2e-16 ***
destination_jatinegara 2.225e+00 6.151e-02 36.175 < 2e-16 ***
destination_johar baru 6.419e-01 1.802e-01 3.562 0.000368 ***
destination_kali deres 1.552e+00 1.013e-01 15.331 < 2e-16 ***
destination_kebayoran baru 2.421e+00 5.782e-02 41.868 < 2e-16 ***
destination_kebayoran lama 2.257e+00 6.043e-02 37.349 < 2e-16 ***
destination_kebon jeruk 1.871e+00 6.318e-02 29.620 < 2e-16 ***
destination_kelapa gading 1.840e+00 7.335e-02 25.080 < 2e-16 ***
destination_kemayoran 1.374e+00 7.707e-02 17.832 < 2e-16 ***
destination_kembangan 1.969e+00 6.540e-02 30.108 < 2e-16 ***
destination_koja 1.853e+00 7.710e-02 24.032 < 2e-16 ***
destination_kramat jati 2.166e+00 6.987e-02 31.002 < 2e-16 ***
destination_makasar 1.732e+00 9.221e-02 18.778 < 2e-16 ***
destination_mampang prapatan 1.776e+00 6.621e-02 26.819 < 2e-16 ***
destination_matraman 1.232e+00 9.030e-02 13.641 < 2e-16 ***
destination_menteng 1.733e+00 6.134e-02 28.247 < 2e-16 ***
destination_pademangan 1.799e+00 6.963e-02 25.836 < 2e-16 ***
destination_palmerah 1.571e+00 6.456e-02 24.332 < 2e-16 ***
destination_pancoran 1.757e+00 6.936e-02 25.334 < 2e-16 ***
destination_pasar minggu 2.258e+00 6.494e-02 34.767 < 2e-16 ***
destination_pasar rebo 1.745e+00 8.416e-02 20.736 < 2e-16 ***
destination_penjaringan 2.482e+00 6.163e-02 40.267 < 2e-16 ***
destination_pesanggrahan 1.680e+00 7.868e-02 21.357 < 2e-16 ***
destination_pulo gadung 1.816e+00 6.962e-02 26.087 < 2e-16 ***
destination_sawah besar 1.329e+00 8.095e-02 16.419 < 2e-16 ***
destination_senen 1.324e+00 7.136e-02 18.549 < 2e-16 ***
destination_setia budi 2.253e+00 5.661e-02 39.790 < 2e-16 ***
destination_taman sari 9.732e-01 8.797e-02 11.063 < 2e-16 ***
destination_tambora 1.150e+00 8.140e-02 14.127 < 2e-16 ***
destination_tanah abang 2.218e+00 5.859e-02 37.859 < 2e-16 ***
destination_tanjung priok 2.137e+00 6.045e-02 35.354 < 2e-16 ***
destination_tebet 2.033e+00 6.064e-02 33.530 < 2e-16 ***
origin_Cultural_Attractions 5.248e-02 1.516e-02 3.462 0.000535 ***
origin_Essentials 3.353e-01 1.862e-02 18.007 < 2e-16 ***
origin_Facilities_Services -1.772e-01 1.889e-02 -9.381 < 2e-16 ***
origin_Offices_Business 8.820e-02 8.129e-03 10.850 < 2e-16 ***
origin_Others 1.025e-01 1.253e-02 8.178 2.89e-16 ***
origin_Restaurants_Food 8.175e-02 1.318e-02 6.201 5.61e-10 ***
origin_Shops -1.214e-02 2.078e-02 -0.584 0.559137
origin_Recreation_Entertainment 3.943e-02 1.144e-02 3.446 0.000570 ***
dist -4.997e-05 6.669e-07 -74.927 < 2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
(Dispersion parameter for poisson family taken to be 1)
Null deviance: 107406.8 on 3759 degrees of freedom
Residual deviance: 5592.6 on 3706 degrees of freedom
AIC: 19586
Number of Fisher Scoring iterations: 5
6.5 Doubly Constrained Spatial Interaction Model.
The Doubly Constrained Spatial Interaction Model considers both the influence of origins and destinations on trip flows, balancing “push” factors from origins and “pull” factors from destinations.
dbcSIM <- glm(formula = trips_count ~ origin_ + destination_ + dist,
family = poisson(link = "log"),
data = tripsData,
na.action = na.exclude)
summary(dbcSIM)
Call:
glm(formula = trips_count ~ origin_ + destination_ + dist, family = poisson(link = "log"),
data = tripsData, na.action = na.exclude)
Coefficients:
Estimate Std. Error z value Pr(>|z|)
(Intercept) 3.203e+00 9.601e-02 33.363 < 2e-16 ***
origin_cempaka putih -4.075e-01 1.003e-01 -4.063 4.85e-05 ***
origin_cengkareng 3.106e-01 9.213e-02 3.371 0.000749 ***
origin_cilandak -1.345e-01 9.759e-02 -1.378 0.168241
origin_cilincing 1.114e-02 1.292e-01 0.086 0.931284
origin_cipayung 3.830e-01 1.097e-01 3.493 0.000477 ***
origin_ciracas 4.487e-02 1.020e-01 0.440 0.660069
origin_danau sunter dll -8.396e-01 3.870e-01 -2.170 0.030042 *
origin_duren sawit 5.814e-01 8.933e-02 6.508 7.62e-11 ***
origin_gambir -4.428e-02 9.000e-02 -0.492 0.622735
origin_grogol petamburan 6.695e-01 8.533e-02 7.846 4.28e-15 ***
origin_jagakarsa 4.331e-01 9.754e-02 4.440 9.00e-06 ***
origin_jatinegara 1.157e-01 8.872e-02 1.304 0.192394
origin_johar baru -7.352e-01 1.313e-01 -5.601 2.14e-08 ***
origin_kali deres -4.729e-03 1.120e-01 -0.042 0.966326
origin_kebayoran baru 4.409e-01 8.557e-02 5.152 2.58e-07 ***
origin_kebayoran lama 2.332e-01 8.804e-02 2.649 0.008083 **
origin_kebon jeruk 3.285e-01 8.797e-02 3.735 0.000188 ***
origin_kelapa gading 5.258e-01 8.776e-02 5.992 2.08e-09 ***
origin_kemayoran -3.082e-01 9.898e-02 -3.114 0.001848 **
origin_kembangan 3.949e-01 9.045e-02 4.365 1.27e-05 ***
origin_koja -2.494e-01 1.122e-01 -2.224 0.026149 *
origin_kramat jati -3.451e-02 9.452e-02 -0.365 0.715013
origin_makasar -4.576e-01 1.200e-01 -3.813 0.000137 ***
origin_mampang prapatan -7.178e-01 1.022e-01 -7.020 2.22e-12 ***
origin_matraman -8.298e-01 1.167e-01 -7.111 1.15e-12 ***
origin_menteng -2.166e-01 8.986e-02 -2.410 0.015950 *
origin_pademangan 1.247e-01 9.598e-02 1.299 0.193935
origin_palmerah -3.711e-01 9.318e-02 -3.982 6.83e-05 ***
origin_pancoran -3.307e-01 9.426e-02 -3.508 0.000451 ***
origin_pasar minggu 5.254e-01 8.826e-02 5.953 2.64e-09 ***
origin_pasar rebo -7.715e-01 1.299e-01 -5.939 2.86e-09 ***
origin_penjaringan 8.009e-01 8.878e-02 9.021 < 2e-16 ***
origin_pesanggrahan -7.365e-01 1.179e-01 -6.246 4.21e-10 ***
origin_pulo gadung -3.143e-02 9.417e-02 -0.334 0.738523
origin_sawah besar -3.119e-01 9.701e-02 -3.215 0.001305 **
origin_senen -2.733e-01 9.313e-02 -2.935 0.003338 **
origin_setia budi 4.776e-01 8.446e-02 5.654 1.57e-08 ***
origin_taman sari -7.840e-02 9.273e-02 -0.845 0.397838
origin_tambora -1.637e-01 9.455e-02 -1.731 0.083468 .
origin_tanah abang 5.135e-01 8.464e-02 6.067 1.31e-09 ***
origin_tanjung priok 6.098e-01 8.885e-02 6.864 6.71e-12 ***
origin_tebet 2.474e-01 8.552e-02 2.892 0.003823 **
destination_cempaka putih -7.762e-01 8.643e-02 -8.981 < 2e-16 ***
destination_cengkareng 1.315e-01 6.191e-02 2.124 0.033645 *
destination_cilandak 3.037e-02 6.413e-02 0.474 0.635828
destination_cilincing -3.308e-01 9.922e-02 -3.334 0.000857 ***
destination_cipayung -2.144e-01 9.910e-02 -2.163 0.030535 *
destination_ciracas -5.476e-01 1.005e-01 -5.450 5.03e-08 ***
destination_danau sunter -1.929e+00 5.029e-01 -3.836 0.000125 ***
destination_danau sunter dll -2.753e+00 7.094e-01 -3.881 0.000104 ***
destination_duren sawit 1.093e-01 7.084e-02 1.543 0.122824
destination_gambir -1.544e-01 5.745e-02 -2.687 0.007208 **
destination_grogol petamburan 8.412e-02 5.837e-02 1.441 0.149552
destination_jagakarsa 4.718e-01 6.575e-02 7.176 7.18e-13 ***
destination_jatinegara 2.465e-02 5.600e-02 0.440 0.659860
destination_johar baru -1.332e+00 1.785e-01 -7.461 8.58e-14 ***
destination_kali deres -5.012e-01 1.013e-01 -4.948 7.49e-07 ***
destination_kebayoran baru 3.172e-01 5.590e-02 5.674 1.39e-08 ***
destination_kebayoran lama 2.051e-01 5.870e-02 3.495 0.000474 ***
destination_kebon jeruk -2.282e-01 6.206e-02 -3.677 0.000236 ***
destination_kelapa gading -3.097e-01 7.089e-02 -4.369 1.25e-05 ***
destination_kemayoran -7.134e-01 7.362e-02 -9.690 < 2e-16 ***
destination_kembangan 1.620e-02 6.478e-02 0.250 0.802565
destination_koja -5.040e-01 7.637e-02 -6.600 4.12e-11 ***
destination_kramat jati -1.293e-01 6.887e-02 -1.877 0.060535 .
destination_makasar -6.126e-01 9.286e-02 -6.597 4.21e-11 ***
destination_mampang prapatan -3.856e-01 6.304e-02 -6.116 9.58e-10 ***
destination_matraman -9.475e-01 8.734e-02 -10.849 < 2e-16 ***
destination_menteng -2.853e-01 5.918e-02 -4.820 1.43e-06 ***
destination_pademangan -2.906e-01 6.688e-02 -4.346 1.39e-05 ***
destination_palmerah -4.769e-01 6.192e-02 -7.702 1.34e-14 ***
destination_pancoran -4.289e-01 6.653e-02 -6.446 1.15e-10 ***
destination_pasar minggu 1.456e-01 6.521e-02 2.232 0.025601 *
destination_pasar rebo -6.262e-01 8.607e-02 -7.276 3.44e-13 ***
destination_penjaringan 5.179e-01 5.917e-02 8.752 < 2e-16 ***
destination_pesanggrahan -5.033e-01 7.834e-02 -6.425 1.32e-10 ***
destination_pulo gadung -4.349e-01 6.679e-02 -6.511 7.45e-11 ***
destination_sawah besar -7.267e-01 7.841e-02 -9.267 < 2e-16 ***
destination_senen -6.100e-01 6.843e-02 -8.915 < 2e-16 ***
destination_setia budi 2.413e-01 5.463e-02 4.417 9.99e-06 ***
destination_taman sari -9.770e-01 8.563e-02 -11.410 < 2e-16 ***
destination_tambora -8.445e-01 7.844e-02 -10.767 < 2e-16 ***
destination_tanah abang 2.175e-01 5.464e-02 3.982 6.84e-05 ***
destination_tanjung priok 7.956e-02 6.002e-02 1.326 0.184978
destination_tebet -4.156e-02 5.709e-02 -0.728 0.466646
dist -5.258e-05 6.921e-07 -75.971 < 2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
(Dispersion parameter for poisson family taken to be 1)
Null deviance: 20530 on 3758 degrees of freedom
Residual deviance: 3537 on 3672 degrees of freedom
AIC: 17598
Number of Fisher Scoring iterations: 5
7.0 Push-Pull Factor Analysis
In our prototyping and hypothesis, there were discussions on whether the number of category counts affected the push and pull factors of that particular district. For example, if a district had was populated with restaurants_and_food POIs as a category, compared to the origin district, we can theorize that residents or tourists might be going there because of said Point of Interest. While we recognize correlation is not causation, we will thus perform push-pull factor analysis to see the distribution and likely reasons why Grab demand might be generated from origin to destination.
7.1 Bar Chart of Coefficients
Steps: 1. We combine coefficients from models into a data frame
origin_constrained_coef <- coef(origSIM)[c("destination_Cultural_Attractions", "destination_Essentials",
"destination_Facilities_Services", "destination_Offices_Business",
"destination_Others", "destination_Restaurants_Food",
"destination_Shops", "destination_Recreation_Entertainment")]
destination_constrained_coef <- coef(destSIM)[c("origin_Cultural_Attractions", "origin_Essentials",
"origin_Facilities_Services", "origin_Offices_Business",
"origin_Others", "origin_Restaurants_Food",
"origin_Shops", "origin_Recreation_Entertainment")]
doubly_constrained_coef <- coef(dbcSIM)[c("origin_Cultural_Attractions", "origin_Essentials",
"origin_Facilities_Services", "origin_Offices_Business",
"origin_Others", "origin_Restaurants_Food",
"origin_Shops", "origin_Recreation_Entertainment")]
coefficients_df <- data.frame(
Category = c("Cultural_Attractions", "Essentials", "Facilities_Services",
"Offices_Business", "Others", "Restaurants_Food",
"Shops", "Recreation_Entertainment"),
Origin_Constrained = origin_constrained_coef,
Destination_Constrained = destination_constrained_coef,
Doubly_Constrained = doubly_constrained_coef
)- We melt the data for ggplot and then plot the coefficients
coefficients_long <- coefficients_df %>%
pivot_longer(cols = -Category, names_to = "Model", values_to = "Coefficient")
# Plot
ggplot(coefficients_long, aes(x = Category, y = Coefficient, fill = Model)) +
geom_bar(stat = "identity", position = "dodge") +
theme_minimal() +
labs(title = "Push-Pull Factor Coefficients by Model",
x = "POI Category",
y = "Coefficient")
- We filter data for each model
origin_constrained_data <- coefficients_long %>% filter(Model == "Origin_Constrained")
destination_constrained_data <- coefficients_long %>% filter(Model == "Destination_Constrained")
num_categories <- length(unique(coefficients_long$Category))
category_colors <- brewer.pal(min(num_categories, 9), "Pastel1")
if (num_categories > 9) {
category_colors <- colorRampPalette(brewer.pal(9, "Pastel1"))(num_categories)
}- Create individual plots for the model. For ease of visualization we rotate the axis and have a centered 0 axis.
plot_origin <- ggplot(origin_constrained_data, aes(y = Category, x = Coefficient, fill = Category)) +
geom_bar(stat = "identity") +
theme_minimal() +
labs(title = "Origin-Constrained Model",
x = "Coefficient",
y = "POI Category") +
theme(axis.text.y = element_text(angle = 0, hjust = 1),
legend.position = "none") + # Remove legend to avoid repetition in the plots
scale_x_continuous(position = "top", limits = c(-max(abs(origin_constrained_data$Coefficient)),
max(abs(origin_constrained_data$Coefficient)))) +
scale_fill_manual(values = category_colors)
plot_destination <- ggplot(destination_constrained_data, aes(y = Category, x = Coefficient, fill = Category)) +
geom_bar(stat = "identity") +
theme_minimal() +
labs(title = "Destination-Constrained Model",
x = "Coefficient",
y = "POI Category") +
theme(axis.text.y = element_text(angle = 0, hjust = 1),
legend.position = "none") + # Remove legend for consistency
scale_x_continuous(position = "top", limits = c(-max(abs(destination_constrained_data$Coefficient)),
max(abs(destination_constrained_data$Coefficient)))) +
scale_fill_manual(values = category_colors)
# Arrange the individual plots in a single column
grid.arrange(plot_origin, plot_destination, ncol = 1)
Other than the comparative Bar Charts above which effectively plots the coefficient of the attractiveness and propulsiveness of the POI categories, we can also consider Chord Diagrams to display the connections between origin and destination POI categories 1. Summarize trips data to get total trips between each origin and destination 2. Clear Previous Plots
chord_data <- tripsData %>%
group_by(origin_, destination_) %>%
summarise(trips_count = sum(trips_count)) %>%
ungroup()`summarise()` has grouped output by 'origin_'. You can override using the
`.groups` argument.
category_colors <- colorRampPalette(c("lightblue", "lightpink", "lightgreen", "lightcoral", "lightyellow"))(length(unique(c(chord_data$origin_, chord_data$destination_))))
circos.clear()- Plot Chord Diagram
chordDiagram(
chord_data,
grid.col = category_colors,
transparency = 0.3,
annotationTrack = "grid",
annotationTrackHeight = mm_h(5),
preAllocateTracks = list(track.height = 0.05) #
)
circos.trackPlotRegion(track.index = 1, panel.fun = function(x, y) {
sector.index <- get.cell.meta.data("sector.index")
circos.text(CELL_META$xcenter, CELL_META$ylim[1] + mm_y(5), sector.index, facing = "clockwise", niceFacing = TRUE, adj = c(0, 0.5), cex = 0.8)
}, bg.border = NA)
title("Push-Pull Factors by Category in Chord Diagram", cex.main = 1.5)
head(top_districts)[1] "setia budi" "grogol petamburan" "kebayoran baru"
[4] "penjaringan" "tanah abang"
head(poi_aggregated_top)# A tibble: 6 × 3
district category count
<chr> <chr> <int>
1 grogol petamburan Cultural_Attractions 8
2 grogol petamburan Essentials 59
3 grogol petamburan Facilities_Services 130
4 grogol petamburan Offices_Business 95
5 grogol petamburan Others 4
6 grogol petamburan Recreation_Entertainment 6
8.0 Proposed Visualizations for OD Desire Line Maps
8.1 Additional Visualization for Trip Count by Time of Day (Heatmap)
To visualize the distribution of trips by time of day, we can use the code chunk below:
ORIGIN
tripsData$origin_time_cluster <- factor(
tripsData$origin_time_cluster,
levels = c("midnight peak", "midnight lull", "morning peak", "morning lull",
"afternoon peak", "afternoon lull", "evening peak", "evening lull")
)
ggplot(tripsData, aes(x = origin_time_cluster, y = origin_, fill = trips_count)) +
geom_tile(color = "white") +
scale_fill_gradient(low = "white", high = "black", name = "Trip Count") +
scale_x_discrete(position = "top") +
labs(
title = "Heatmap of Trips Throughout the Time of Day",
x = "Time Cluster",
y = "Destination District"
) +
theme_void() +
theme(
axis.text.x = element_text(angle = 0, hjust = 0.5),
axis.text.y = element_text(margin = margin(r = 5)),
plot.title = element_text(hjust = 0.5)
)
DESTINATION
tripsData$origin_time_cluster <- factor(
tripsData$origin_time_cluster,
levels = c("midnight peak", "midnight lull", "morning peak", "morning lull",
"afternoon peak", "afternoon lull", "evening peak", "evening lull")
)
ggplot(tripsData, aes(x = origin_time_cluster, y = destination_, fill = trips_count)) +
geom_tile(color = "white") +
scale_fill_gradient(low = "white", high = "black", name = "Trip Count") + # Grayscale palette
scale_x_discrete(position = "top") +
labs(
title = "Heatmap of Trips Throughout the Time of Day",
x = "Time Cluster",
y = "Destination District"
) +
theme_void() + # Removes all default grid elements
theme(
axis.text.x = element_text(angle = 0, hjust = 0.5),
axis.text.y = element_text(margin = margin(r = 5)),
plot.title = element_text(hjust = 0.5)
)
8.2 Split Plot Visualization for Weather Conditions:
- We create a central 0 line
- Represent ‘Not_Rain’ with Grey and ‘Rain’ with Blue
trip_counts_district <- tripsData %>%
group_by(origin_, origin_weather_description_category) %>%
summarise(trip_count = n(), .groups = "drop")
trip_counts_district <- trip_counts_district %>%
mutate(trip_count = ifelse(origin_weather_description_category == "rain", -trip_count, trip_count))
ggplot(trip_counts_district, aes(y = origin_, x = trip_count, fill = origin_weather_description_category)) +
geom_bar(stat = "identity", width = 0.8) +
labs(
title = "Trip Counts by Rain Condition per District",
y = "District",
x = "Trip Count",
fill = "Weather Condition"
) +
theme_minimal() +
theme(
axis.text.y = element_text(size = 8, margin = margin(r = 10)),
panel.grid = element_blank()
) +
scale_fill_manual(values = c("darkgrey", "blue")) +
scale_x_continuous(labels = abs, limits = c(-max(trip_counts_district$trip_count), max(trip_counts_district$trip_count))) + geom_vline(xintercept = 0, color = "black") 
8.3 Shiny Prototype Sample: Time Cluster (with Vehicle Type)

8.4 Shiny Prototype Sample: Weather Condition with Split Plot Visualization

8.5 Shiny Prototype Sample: Points of Interest + Push and Pull Factor Analysis

(To be Combined with Selected Desire Line Map Conditions)
For Prototyping Purposes we depict a singular point to see the tooltip and distribution of categories
For the Chord Diagram, Upon Clicking on a Specific District in the Radius, the user can visualize the flows of trips, in this Case District as Origin
9.0 Conclusion
In this project, I set out to understand the demand for Grab’s ride-hailing services in Jakarta by digging into spatial patterns and exploring how factors like time of day, weather, and vehicle type impact trip flows across the city. By analyzing demand patterns around specific Points of Interest (like restaurants, entertainment spots, and essential services), I found that these areas attract more trips during peak times and when it rains—insights that could help Grab, urban planners, and policymakers better manage the challenges of Jakarta’s traffic.
Geospatial Analysis, especially Spatial Interaction Modelling can show how smart data use can make city life smoother. By using visualizations like heatmaps and chord diagrams, I tried to break down complex mobility patterns into something more accessible. The insights here could help shape practical solutions for Jakarta’s traffic congestion, from adjusting service availability during high-demand times to making it easier for people to reach essential services without exacerbating traffic congestion. While this analysis may not offer direct solutions, it does offer insights based on complex geospatial variables.